Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 14 additions & 132 deletions agentuity/otel/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
import logging
import signal
import os
from agentuity import __version__
from typing import Optional, Dict
from opentelemetry import trace
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry import metrics
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.trace_exporter import Compression
from opentelemetry import _logs
from opentelemetry.sdk._logs import LoggingHandler, LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.propagate import set_global_textmap
from .logfilter import ModuleFilter
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make OTEL an optional dependency (protect import).

Importing SERVICE_NAME/SERVICE_VERSION at module import time breaks environments without OpenTelemetry installed. Guard it and fall back to string keys.

-from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION
+try:
+    from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION
+except Exception:
+    # Fallback to canonical attribute names when OTEL isn't installed
+    SERVICE_NAME = "service.name"
+    SERVICE_VERSION = "service.version"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION
try:
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION
except Exception:
# Fallback to canonical attribute names when OTEL isn't installed
SERVICE_NAME = "service.name"
SERVICE_VERSION = "service.version"
🤖 Prompt for AI Agents
In agentuity/otel/__init__.py around line 5, importing SERVICE_NAME and
SERVICE_VERSION from opentelemetry at module import time will raise when
OpenTelemetry isn't installed; wrap the import in a try/except ImportError and
on ImportError assign SERVICE_NAME and SERVICE_VERSION to the literal string
keys you expect to use (e.g. "service.name" and "service.version"), ensuring the
module still exposes those names for downstream code without requiring the
opentelemetry dependency.

from .logger import create_logger
from .span_patch import patch_span

Expand Down Expand Up @@ -69,12 +52,14 @@ def init(config: Optional[Dict[str, str]] = {}):
app_version = config.get(
"app_version", os.environ.get("AGENTUITY_SDK_APP_VERSION", "unknown")
)
export_internal_ms = 500 if devmode else 60000
max_export_batch_size = 1 if devmode else 512
schedule_delay_millis = 500 if devmode else 30000

resource = Resource(
attributes={
# Initialize traceloop for automatic instrumentation
try:
from traceloop.sdk import Traceloop

headers = {"Authorization": f"Bearer {bearer_token}"} if bearer_token else {}

resource_attributes = {
SERVICE_NAME: config.get(
"service_name",
app_name,
Expand All @@ -91,120 +76,17 @@ def init(config: Optional[Dict[str, str]] = {}):
"@agentuity/sdkVersion": sdkVersion,
"@agentuity/cliVersion": cliVersion,
"@agentuity/language": "python",
"env": "dev" if devmode else "production",
"version": __version__,
}
)

headers = {
"Authorization": "Bearer " + bearer_token,
}

tracerProvider = TracerProvider(
resource=resource,
shutdown_on_exit=False,
)
exporter = OTLPSpanExporter(
endpoint=endpoint + "/v1/traces",
headers=headers,
compression=Compression.Gzip,
timeout=10,
)
processor = BatchSpanProcessor(
exporter,
export_timeout_millis=export_internal_ms,
max_export_batch_size=max_export_batch_size,
schedule_delay_millis=schedule_delay_millis,
)

if os.environ.get("AGENTUITY_OTLP_CONSOLE_EXPORTER", "false") == "true":
tracerProvider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))

tracerProvider.add_span_processor(processor)
trace.set_tracer_provider(tracerProvider)

reader = PeriodicExportingMetricReader(
OTLPMetricExporter(
endpoint=endpoint + "/v1/metrics",
headers=headers,
compression=Compression.Gzip,
timeout=10,
),
export_interval_millis=export_internal_ms,
)
meterProvider = MeterProvider(
resource=resource,
metric_readers=[reader],
shutdown_on_exit=False,
)
metrics.set_meter_provider(meterProvider)

# Set up logging
loggerProvider = LoggerProvider(resource=resource)
logProcessor = BatchLogRecordProcessor(
OTLPLogExporter(
endpoint=endpoint + "/v1/logs",
headers=headers,
compression=Compression.Gzip,
timeout=10,
),
max_export_batch_size=max_export_batch_size,
export_timeout_millis=export_internal_ms,
schedule_delay_millis=schedule_delay_millis,
)
loggerProvider.add_log_record_processor(logProcessor)
_logs.set_logger_provider(loggerProvider)

handler = LoggingHandler(
level=logging.NOTSET,
logger_provider=loggerProvider,
)
module_filter = ModuleFilter()
handler.addFilter(module_filter)

root_logger = logging.getLogger()
root_logger.addHandler(handler)

propagator = TraceContextTextMapPropagator()
set_global_textmap(propagator)

stopped = False

def signal_handler(sig, frame):
nonlocal stopped
if stopped:
return
stopped = True
logProcessor.force_flush()
meterProvider.force_flush()
tracerProvider.force_flush()
meterProvider.shutdown()
tracerProvider.shutdown()
logProcessor.shutdown()

# Register signal handler for graceful shutdown
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

# Initialize traceloop for automatic instrumentation
try:
from traceloop.sdk import Traceloop

# Build app name from project and agent info if available
project_name = config.get("project_name", "")
agent_name = config.get("agent_name", "")
app_name = f"{project_name}:{agent_name}"

headers = {"Authorization": f"Bearer {bearer_token}"} if bearer_token else {}

Traceloop.init(
app_name=app_name,
api_endpoint=endpoint,
headers=headers,
disable_batch=devmode, # Only disable batching in dev mode
telemetry_enabled=False, # Don't send any data to Traceloop
resource_attributes={
"env": "dev" if devmode else "production",
"version": __version__,
},
disable_batch=devmode,
resource_attributes=resource_attributes,
telemetry_enabled=False
)
logger.debug(f"Traceloop initialized with app_name: {app_name}")
logger.info("Traceloop configured successfully")
Expand All @@ -213,7 +95,7 @@ def signal_handler(sig, frame):
except Exception as e:
logger.warning(f"Failed to configure Traceloop: {e}, continuing without it")

return handler
return None


__all__ = ["init", "create_logger"]
60 changes: 7 additions & 53 deletions tests/otel/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
from unittest.mock import patch, MagicMock
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION
from opentelemetry.exporter.otlp.proto.http.trace_exporter import Compression


sys.modules["openlit"] = MagicMock()
from agentuity.otel import init # noqa: E402
Expand Down Expand Up @@ -72,62 +72,16 @@ def test_init_with_config(self):
}

with (
patch("agentuity.otel.TracerProvider") as mock_tracer_provider,
patch("agentuity.otel.OTLPSpanExporter") as mock_span_exporter,
patch("agentuity.otel.BatchSpanProcessor"),
patch("agentuity.otel.trace.set_tracer_provider") as mock_set_tracer,
patch("agentuity.otel.PeriodicExportingMetricReader"),
patch("agentuity.otel.OTLPMetricExporter"),
patch("agentuity.otel.MeterProvider") as mock_meter_provider,
patch("agentuity.otel.metrics.set_meter_provider") as mock_set_meter,
patch("agentuity.otel.LoggerProvider") as mock_logger_provider,
patch("agentuity.otel.BatchLogRecordProcessor"),
patch("agentuity.otel.OTLPLogExporter"),
patch("agentuity.otel._logs.set_logger_provider") as mock_set_logger,
patch("agentuity.otel.LoggingHandler") as mock_logging_handler,
patch("agentuity.otel.ModuleFilter"),
patch("agentuity.otel.TraceContextTextMapPropagator"),
patch("agentuity.otel.set_global_textmap") as mock_set_textmap,
patch("agentuity.otel.signal.signal") as mock_signal,
patch("traceloop.sdk.Traceloop.init") as mock_traceloop_init,
patch("agentuity.otel.logger"),
patch("agentuity.otel.logging.getLogger") as mock_get_logger,
):
mock_root_logger = MagicMock()
mock_get_logger.return_value = mock_root_logger

mock_handler_instance = MagicMock()
mock_logging_handler.return_value = mock_handler_instance

result = init(config)

assert result is mock_handler_instance

mock_tracer_provider.assert_called_once()
resource_arg = mock_tracer_provider.call_args[1]["resource"]
assert resource_arg.attributes[SERVICE_NAME] == "test_service"
assert resource_arg.attributes[SERVICE_VERSION] == "1.0.0"

mock_span_exporter.assert_called_once_with(
endpoint="https://test.com/v1/traces",
headers={"Authorization": "Bearer test_token"},
compression=Compression.Gzip,
timeout=10,
)

mock_set_tracer.assert_called_once()

mock_meter_provider.assert_called_once()
mock_set_meter.assert_called_once()

mock_logger_provider.assert_called_once()
mock_set_logger.assert_called_once()

mock_logging_handler.assert_called_once()
mock_root_logger.addHandler.assert_called_once_with(mock_handler_instance)

mock_set_textmap.assert_called_once()

assert mock_signal.call_count == 2
assert result is None

mock_traceloop_init.assert_called_once()
args, kwargs = mock_traceloop_init.call_args
assert kwargs["api_endpoint"] == "https://test.com"
assert kwargs["headers"] == {"Authorization": "Bearer test_token"}
assert kwargs["resource_attributes"][SERVICE_NAME] == "test_service"
assert kwargs["resource_attributes"][SERVICE_VERSION] == "1.0.0"