botanu 0.1.dev60__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- botanu/__init__.py +76 -0
- botanu/_version.py +13 -0
- botanu/integrations/__init__.py +4 -0
- botanu/integrations/tenacity.py +60 -0
- botanu/models/__init__.py +10 -0
- botanu/models/run_context.py +328 -0
- botanu/processors/__init__.py +12 -0
- botanu/processors/enricher.py +84 -0
- botanu/py.typed +0 -0
- botanu/resources/__init__.py +87 -0
- botanu/sdk/__init__.py +37 -0
- botanu/sdk/bootstrap.py +405 -0
- botanu/sdk/config.py +330 -0
- botanu/sdk/context.py +73 -0
- botanu/sdk/decorators.py +407 -0
- botanu/sdk/middleware.py +97 -0
- botanu/sdk/span_helpers.py +143 -0
- botanu/tracking/__init__.py +55 -0
- botanu/tracking/data.py +488 -0
- botanu/tracking/llm.py +700 -0
- botanu-0.1.dev60.dist-info/METADATA +208 -0
- botanu-0.1.dev60.dist-info/RECORD +25 -0
- botanu-0.1.dev60.dist-info/WHEEL +4 -0
- botanu-0.1.dev60.dist-info/licenses/LICENSE +200 -0
- botanu-0.1.dev60.dist-info/licenses/NOTICE +17 -0
botanu/sdk/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Botanu SDK core components."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from botanu.sdk.bootstrap import disable, enable, get_config, is_enabled
|
|
9
|
+
from botanu.sdk.config import BotanuConfig
|
|
10
|
+
from botanu.sdk.context import (
|
|
11
|
+
get_baggage,
|
|
12
|
+
get_current_span,
|
|
13
|
+
get_run_id,
|
|
14
|
+
get_workflow,
|
|
15
|
+
set_baggage,
|
|
16
|
+
)
|
|
17
|
+
from botanu.sdk.decorators import botanu_outcome, botanu_workflow, run_botanu, workflow
|
|
18
|
+
from botanu.sdk.span_helpers import emit_outcome, set_business_context
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BotanuConfig",
|
|
22
|
+
"botanu_outcome",
|
|
23
|
+
"botanu_workflow",
|
|
24
|
+
"disable",
|
|
25
|
+
"emit_outcome",
|
|
26
|
+
"enable",
|
|
27
|
+
"get_baggage",
|
|
28
|
+
"get_config",
|
|
29
|
+
"get_current_span",
|
|
30
|
+
"get_run_id",
|
|
31
|
+
"get_workflow",
|
|
32
|
+
"is_enabled",
|
|
33
|
+
"run_botanu",
|
|
34
|
+
"set_baggage",
|
|
35
|
+
"set_business_context",
|
|
36
|
+
"workflow",
|
|
37
|
+
]
|
botanu/sdk/bootstrap.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 The Botanu Authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Botanu Bootstrap — one-switch enablement for OTEL auto-instrumentation.
|
|
5
|
+
|
|
6
|
+
This is the "Botanu OTel Distribution" — a curated bundle that:
|
|
7
|
+
|
|
8
|
+
1. Configures OTEL SDK with OTLP exporter
|
|
9
|
+
2. Enables OTEL auto-instrumentation for popular libraries
|
|
10
|
+
3. Adds :class:`~botanu.processors.enricher.RunContextEnricher`
|
|
11
|
+
(propagates ``run_id`` to all spans)
|
|
12
|
+
4. Sets up W3C TraceContext + Baggage propagators
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
from botanu import enable
|
|
17
|
+
enable() # reads OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT from env
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import threading
|
|
25
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from botanu.sdk.config import BotanuConfig
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_lock = threading.RLock()
|
|
33
|
+
_initialized = False
|
|
34
|
+
_current_config: Optional[BotanuConfig] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def enable(
|
|
38
|
+
service_name: Optional[str] = None,
|
|
39
|
+
otlp_endpoint: Optional[str] = None,
|
|
40
|
+
environment: Optional[str] = None,
|
|
41
|
+
auto_instrumentation: bool = True,
|
|
42
|
+
propagators: Optional[List[str]] = None,
|
|
43
|
+
log_level: str = "INFO",
|
|
44
|
+
config: Optional[BotanuConfig] = None,
|
|
45
|
+
config_file: Optional[str] = None,
|
|
46
|
+
) -> bool:
|
|
47
|
+
"""Enable Botanu SDK with OTEL auto-instrumentation.
|
|
48
|
+
|
|
49
|
+
This is the ONE function customers need to call to get full observability.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
service_name: Service name.
|
|
53
|
+
otlp_endpoint: OTLP collector endpoint.
|
|
54
|
+
environment: Deployment environment.
|
|
55
|
+
auto_instrumentation: Enable OTEL auto-instrumentation (default: ``True``).
|
|
56
|
+
propagators: List of propagators (default: ``["tracecontext", "baggage"]``).
|
|
57
|
+
log_level: Logging level (default: ``"INFO"``).
|
|
58
|
+
config: Full :class:`BotanuConfig` (overrides individual params).
|
|
59
|
+
config_file: Path to YAML config file.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
``True`` if successfully initialized, ``False`` if already initialized.
|
|
63
|
+
"""
|
|
64
|
+
global _initialized, _current_config
|
|
65
|
+
|
|
66
|
+
with _lock:
|
|
67
|
+
if _initialized:
|
|
68
|
+
logger.warning("Botanu SDK already initialized")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
logging.basicConfig(level=getattr(logging, log_level.upper()))
|
|
72
|
+
|
|
73
|
+
from botanu.sdk.config import BotanuConfig as ConfigClass
|
|
74
|
+
|
|
75
|
+
if config is not None:
|
|
76
|
+
cfg = config
|
|
77
|
+
elif config_file is not None:
|
|
78
|
+
cfg = ConfigClass.from_yaml(config_file)
|
|
79
|
+
else:
|
|
80
|
+
cfg = ConfigClass.from_file_or_env()
|
|
81
|
+
|
|
82
|
+
if service_name is not None:
|
|
83
|
+
cfg.service_name = service_name
|
|
84
|
+
if otlp_endpoint is not None:
|
|
85
|
+
cfg.otlp_endpoint = otlp_endpoint
|
|
86
|
+
if environment is not None:
|
|
87
|
+
cfg.deployment_environment = environment
|
|
88
|
+
|
|
89
|
+
_current_config = cfg
|
|
90
|
+
|
|
91
|
+
traces_endpoint = cfg.otlp_endpoint
|
|
92
|
+
if traces_endpoint and not traces_endpoint.endswith("/v1/traces"):
|
|
93
|
+
traces_endpoint = f"{traces_endpoint.rstrip('/')}/v1/traces"
|
|
94
|
+
|
|
95
|
+
otel_sampler_env = os.getenv("OTEL_TRACES_SAMPLER")
|
|
96
|
+
if otel_sampler_env and otel_sampler_env != "always_on":
|
|
97
|
+
logger.warning(
|
|
98
|
+
"OTEL_TRACES_SAMPLER=%s is set but Botanu enforces ALWAYS_ON. No spans will be sampled or dropped.",
|
|
99
|
+
otel_sampler_env,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
logger.info(
|
|
103
|
+
"Initializing Botanu SDK: service=%s, env=%s, endpoint=%s",
|
|
104
|
+
cfg.service_name,
|
|
105
|
+
cfg.deployment_environment,
|
|
106
|
+
traces_endpoint,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
from opentelemetry import trace
|
|
111
|
+
from opentelemetry.baggage.propagation import W3CBaggagePropagator
|
|
112
|
+
from opentelemetry.propagate import set_global_textmap
|
|
113
|
+
from opentelemetry.propagators.composite import CompositePropagator
|
|
114
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
115
|
+
except ImportError as exc:
|
|
116
|
+
logger.error("Missing opentelemetry-api. Install with: pip install botanu")
|
|
117
|
+
raise ImportError("opentelemetry-api is required. Install with: pip install botanu") from exc
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
121
|
+
from opentelemetry.sdk.resources import Resource
|
|
122
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
123
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
124
|
+
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
|
|
125
|
+
except ImportError as exc:
|
|
126
|
+
logger.error("Missing OTel SDK dependencies. Install with: pip install botanu")
|
|
127
|
+
raise ImportError("OTel SDK and exporter required for enable(). Install with: pip install botanu") from exc
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
from botanu._version import __version__
|
|
131
|
+
from botanu.processors import RunContextEnricher
|
|
132
|
+
from botanu.resources import detect_resource_attrs
|
|
133
|
+
|
|
134
|
+
resource_attrs = {
|
|
135
|
+
"service.name": cfg.service_name,
|
|
136
|
+
"deployment.environment": cfg.deployment_environment,
|
|
137
|
+
"telemetry.sdk.name": "botanu",
|
|
138
|
+
"telemetry.sdk.version": __version__,
|
|
139
|
+
}
|
|
140
|
+
if cfg.service_version:
|
|
141
|
+
resource_attrs["service.version"] = cfg.service_version
|
|
142
|
+
if cfg.service_namespace:
|
|
143
|
+
resource_attrs["service.namespace"] = cfg.service_namespace
|
|
144
|
+
|
|
145
|
+
if cfg.auto_detect_resources:
|
|
146
|
+
detected = detect_resource_attrs()
|
|
147
|
+
for key, value in detected.items():
|
|
148
|
+
if key not in resource_attrs:
|
|
149
|
+
resource_attrs[key] = value
|
|
150
|
+
if detected:
|
|
151
|
+
logger.debug("Auto-detected resources: %s", list(detected.keys()))
|
|
152
|
+
|
|
153
|
+
resource = Resource.create(resource_attrs)
|
|
154
|
+
|
|
155
|
+
provider = TracerProvider(resource=resource, sampler=ALWAYS_ON)
|
|
156
|
+
trace.set_tracer_provider(provider)
|
|
157
|
+
|
|
158
|
+
lean_mode = cfg.propagation_mode == "lean"
|
|
159
|
+
provider.add_span_processor(RunContextEnricher(lean_mode=lean_mode))
|
|
160
|
+
|
|
161
|
+
exporter = OTLPSpanExporter(
|
|
162
|
+
endpoint=traces_endpoint,
|
|
163
|
+
headers=cfg.otlp_headers or {},
|
|
164
|
+
)
|
|
165
|
+
provider.add_span_processor(
|
|
166
|
+
BatchSpanProcessor(
|
|
167
|
+
exporter,
|
|
168
|
+
max_export_batch_size=cfg.max_export_batch_size,
|
|
169
|
+
max_queue_size=cfg.max_queue_size,
|
|
170
|
+
schedule_delay_millis=cfg.schedule_delay_millis,
|
|
171
|
+
export_timeout_millis=cfg.export_timeout_millis,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
set_global_textmap(
|
|
176
|
+
CompositePropagator(
|
|
177
|
+
[
|
|
178
|
+
TraceContextTextMapPropagator(),
|
|
179
|
+
W3CBaggagePropagator(),
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
logger.info("Botanu SDK tracing initialized")
|
|
185
|
+
|
|
186
|
+
# Set up LoggerProvider for outcome event emission
|
|
187
|
+
try:
|
|
188
|
+
from opentelemetry._logs import set_logger_provider
|
|
189
|
+
from opentelemetry.sdk._logs import LoggerProvider as _LoggerProvider
|
|
190
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
191
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
192
|
+
|
|
193
|
+
logs_endpoint = cfg.otlp_endpoint
|
|
194
|
+
if logs_endpoint and not logs_endpoint.endswith("/v1/logs"):
|
|
195
|
+
logs_endpoint = f"{logs_endpoint.rstrip('/')}/v1/logs"
|
|
196
|
+
|
|
197
|
+
log_provider = _LoggerProvider(resource=resource)
|
|
198
|
+
log_exporter = OTLPLogExporter(endpoint=logs_endpoint, headers=cfg.otlp_headers or {})
|
|
199
|
+
log_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
|
|
200
|
+
set_logger_provider(log_provider)
|
|
201
|
+
logger.info("Botanu SDK log provider initialized")
|
|
202
|
+
except ImportError:
|
|
203
|
+
logger.debug("OTel log exporter not available; outcome log emission disabled")
|
|
204
|
+
|
|
205
|
+
if auto_instrumentation:
|
|
206
|
+
_enable_auto_instrumentation()
|
|
207
|
+
|
|
208
|
+
_initialized = True
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
logger.error("Failed to initialize Botanu SDK: %s", exc, exc_info=True)
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _enable_auto_instrumentation() -> None:
|
|
217
|
+
"""Enable OTEL auto-instrumentation for common libraries.
|
|
218
|
+
|
|
219
|
+
Each instrumentation is optional — if the underlying library or
|
|
220
|
+
instrumentation package isn't installed, it is silently skipped.
|
|
221
|
+
"""
|
|
222
|
+
enabled: List[str] = []
|
|
223
|
+
failed: List[tuple[str, str]] = []
|
|
224
|
+
|
|
225
|
+
# ── HTTP clients ──────────────────────────────────────────────
|
|
226
|
+
_try_instrument(enabled, failed, "httpx", "opentelemetry.instrumentation.httpx", "HTTPXClientInstrumentor")
|
|
227
|
+
_try_instrument(enabled, failed, "requests", "opentelemetry.instrumentation.requests", "RequestsInstrumentor")
|
|
228
|
+
_try_instrument(enabled, failed, "urllib3", "opentelemetry.instrumentation.urllib3", "URLLib3Instrumentor")
|
|
229
|
+
_try_instrument(enabled, failed, "urllib", "opentelemetry.instrumentation.urllib", "URLLibInstrumentor")
|
|
230
|
+
_try_instrument(
|
|
231
|
+
enabled, failed, "aiohttp_client", "opentelemetry.instrumentation.aiohttp_client", "AioHttpClientInstrumentor"
|
|
232
|
+
)
|
|
233
|
+
_try_instrument(
|
|
234
|
+
enabled, failed, "aiohttp_server", "opentelemetry.instrumentation.aiohttp_server", "AioHttpServerInstrumentor"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# ── Web frameworks ────────────────────────────────────────────
|
|
238
|
+
_try_instrument(enabled, failed, "fastapi", "opentelemetry.instrumentation.fastapi", "FastAPIInstrumentor")
|
|
239
|
+
_try_instrument(enabled, failed, "flask", "opentelemetry.instrumentation.flask", "FlaskInstrumentor")
|
|
240
|
+
_try_instrument(enabled, failed, "django", "opentelemetry.instrumentation.django", "DjangoInstrumentor")
|
|
241
|
+
_try_instrument(enabled, failed, "starlette", "opentelemetry.instrumentation.starlette", "StarletteInstrumentor")
|
|
242
|
+
_try_instrument(enabled, failed, "falcon", "opentelemetry.instrumentation.falcon", "FalconInstrumentor")
|
|
243
|
+
_try_instrument(enabled, failed, "pyramid", "opentelemetry.instrumentation.pyramid", "PyramidInstrumentor")
|
|
244
|
+
_try_instrument(enabled, failed, "tornado", "opentelemetry.instrumentation.tornado", "TornadoInstrumentor")
|
|
245
|
+
|
|
246
|
+
# ── Databases ─────────────────────────────────────────────────
|
|
247
|
+
_try_instrument(enabled, failed, "sqlalchemy", "opentelemetry.instrumentation.sqlalchemy", "SQLAlchemyInstrumentor")
|
|
248
|
+
_try_instrument(enabled, failed, "psycopg2", "opentelemetry.instrumentation.psycopg2", "Psycopg2Instrumentor")
|
|
249
|
+
_try_instrument(enabled, failed, "psycopg", "opentelemetry.instrumentation.psycopg", "PsycopgInstrumentor")
|
|
250
|
+
_try_instrument(enabled, failed, "asyncpg", "opentelemetry.instrumentation.asyncpg", "AsyncPGInstrumentor")
|
|
251
|
+
_try_instrument(enabled, failed, "aiopg", "opentelemetry.instrumentation.aiopg", "AiopgInstrumentor")
|
|
252
|
+
_try_instrument(enabled, failed, "pymongo", "opentelemetry.instrumentation.pymongo", "PymongoInstrumentor")
|
|
253
|
+
_try_instrument(enabled, failed, "redis", "opentelemetry.instrumentation.redis", "RedisInstrumentor")
|
|
254
|
+
_try_instrument(enabled, failed, "mysql", "opentelemetry.instrumentation.mysql", "MySQLInstrumentor")
|
|
255
|
+
_try_instrument(
|
|
256
|
+
enabled, failed, "mysqlclient", "opentelemetry.instrumentation.mysqlclient", "MySQLClientInstrumentor"
|
|
257
|
+
)
|
|
258
|
+
_try_instrument(enabled, failed, "pymysql", "opentelemetry.instrumentation.pymysql", "PyMySQLInstrumentor")
|
|
259
|
+
_try_instrument(enabled, failed, "sqlite3", "opentelemetry.instrumentation.sqlite3", "SQLite3Instrumentor")
|
|
260
|
+
_try_instrument(
|
|
261
|
+
enabled, failed, "elasticsearch", "opentelemetry.instrumentation.elasticsearch", "ElasticsearchInstrumentor"
|
|
262
|
+
)
|
|
263
|
+
_try_instrument(enabled, failed, "cassandra", "opentelemetry.instrumentation.cassandra", "CassandraInstrumentor")
|
|
264
|
+
_try_instrument(
|
|
265
|
+
enabled, failed, "tortoise_orm", "opentelemetry.instrumentation.tortoiseorm", "TortoiseORMInstrumentor"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# ── Caching ───────────────────────────────────────────────────
|
|
269
|
+
_try_instrument(enabled, failed, "pymemcache", "opentelemetry.instrumentation.pymemcache", "PymemcacheInstrumentor")
|
|
270
|
+
|
|
271
|
+
# ── Messaging / Task queues ───────────────────────────────────
|
|
272
|
+
_try_instrument(enabled, failed, "celery", "opentelemetry.instrumentation.celery", "CeleryInstrumentor")
|
|
273
|
+
_try_instrument(enabled, failed, "kafka-python", "opentelemetry.instrumentation.kafka_python", "KafkaInstrumentor")
|
|
274
|
+
_try_instrument(
|
|
275
|
+
enabled,
|
|
276
|
+
failed,
|
|
277
|
+
"confluent-kafka",
|
|
278
|
+
"opentelemetry.instrumentation.confluent_kafka",
|
|
279
|
+
"ConfluentKafkaInstrumentor",
|
|
280
|
+
)
|
|
281
|
+
_try_instrument(enabled, failed, "aiokafka", "opentelemetry.instrumentation.aiokafka", "AioKafkaInstrumentor")
|
|
282
|
+
_try_instrument(enabled, failed, "pika", "opentelemetry.instrumentation.pika", "PikaInstrumentor")
|
|
283
|
+
_try_instrument(enabled, failed, "aio-pika", "opentelemetry.instrumentation.aio_pika", "AioPikaInstrumentor")
|
|
284
|
+
|
|
285
|
+
# ── AWS ───────────────────────────────────────────────────────
|
|
286
|
+
_try_instrument(enabled, failed, "botocore", "opentelemetry.instrumentation.botocore", "BotocoreInstrumentor")
|
|
287
|
+
_try_instrument(enabled, failed, "boto3sqs", "opentelemetry.instrumentation.boto3sqs", "Boto3SQSInstrumentor")
|
|
288
|
+
|
|
289
|
+
# ── gRPC ──────────────────────────────────────────────────────
|
|
290
|
+
_try_instrument_grpc(enabled, failed)
|
|
291
|
+
|
|
292
|
+
# ── GenAI / AI ────────────────────────────────────────────────
|
|
293
|
+
_try_instrument(enabled, failed, "openai", "opentelemetry.instrumentation.openai_v2", "OpenAIInstrumentor")
|
|
294
|
+
_try_instrument(enabled, failed, "anthropic", "opentelemetry.instrumentation.anthropic", "AnthropicInstrumentor")
|
|
295
|
+
_try_instrument(enabled, failed, "vertexai", "opentelemetry.instrumentation.vertexai", "VertexAIInstrumentor")
|
|
296
|
+
_try_instrument(
|
|
297
|
+
enabled,
|
|
298
|
+
failed,
|
|
299
|
+
"google_genai",
|
|
300
|
+
"opentelemetry.instrumentation.google_generativeai",
|
|
301
|
+
"GoogleGenerativeAIInstrumentor",
|
|
302
|
+
)
|
|
303
|
+
_try_instrument(enabled, failed, "langchain", "opentelemetry.instrumentation.langchain", "LangchainInstrumentor")
|
|
304
|
+
_try_instrument(enabled, failed, "ollama", "opentelemetry.instrumentation.ollama", "OllamaInstrumentor")
|
|
305
|
+
_try_instrument(enabled, failed, "crewai", "opentelemetry.instrumentation.crewai", "CrewAIInstrumentor")
|
|
306
|
+
|
|
307
|
+
# ── Runtime / Concurrency ─────────────────────────────────────
|
|
308
|
+
_try_instrument(enabled, failed, "logging", "opentelemetry.instrumentation.logging", "LoggingInstrumentor")
|
|
309
|
+
_try_instrument(enabled, failed, "threading", "opentelemetry.instrumentation.threading", "ThreadingInstrumentor")
|
|
310
|
+
_try_instrument(enabled, failed, "asyncio", "opentelemetry.instrumentation.asyncio", "AsyncioInstrumentor")
|
|
311
|
+
|
|
312
|
+
if enabled:
|
|
313
|
+
logger.info("Auto-instrumentation enabled: %s", ", ".join(enabled))
|
|
314
|
+
if failed:
|
|
315
|
+
for name, error in failed:
|
|
316
|
+
logger.warning("Auto-instrumentation failed for %s: %s", name, error)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _try_instrument(
|
|
320
|
+
enabled: List[str],
|
|
321
|
+
failed: List[tuple[str, str]],
|
|
322
|
+
name: str,
|
|
323
|
+
module_path: str,
|
|
324
|
+
class_name: str,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Try to import and instrument a single library."""
|
|
327
|
+
try:
|
|
328
|
+
import importlib
|
|
329
|
+
|
|
330
|
+
mod = importlib.import_module(module_path)
|
|
331
|
+
instrumentor_cls = getattr(mod, class_name)
|
|
332
|
+
instrumentor_cls().instrument()
|
|
333
|
+
enabled.append(name)
|
|
334
|
+
except ImportError:
|
|
335
|
+
pass
|
|
336
|
+
except Exception as exc:
|
|
337
|
+
failed.append((name, str(exc)))
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _try_instrument_grpc(
|
|
341
|
+
enabled: List[str],
|
|
342
|
+
failed: List[tuple[str, str]],
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Try to instrument gRPC (client + server)."""
|
|
345
|
+
try:
|
|
346
|
+
from opentelemetry.instrumentation.grpc import (
|
|
347
|
+
GrpcInstrumentorClient,
|
|
348
|
+
GrpcInstrumentorServer,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
GrpcInstrumentorClient().instrument()
|
|
352
|
+
GrpcInstrumentorServer().instrument()
|
|
353
|
+
enabled.append("grpc")
|
|
354
|
+
except ImportError:
|
|
355
|
+
pass
|
|
356
|
+
except Exception as exc:
|
|
357
|
+
failed.append(("grpc", str(exc)))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def is_enabled() -> bool:
|
|
361
|
+
"""Check if Botanu SDK is initialized."""
|
|
362
|
+
return _initialized
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def get_config() -> Optional[BotanuConfig]:
|
|
366
|
+
"""Get the current Botanu configuration."""
|
|
367
|
+
return _current_config
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def disable() -> None:
|
|
371
|
+
"""Disable Botanu SDK and shutdown OTEL.
|
|
372
|
+
|
|
373
|
+
Call on application shutdown for clean exit.
|
|
374
|
+
"""
|
|
375
|
+
global _initialized, _current_config
|
|
376
|
+
|
|
377
|
+
with _lock:
|
|
378
|
+
if not _initialized:
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
from opentelemetry import trace
|
|
383
|
+
|
|
384
|
+
provider = trace.get_tracer_provider()
|
|
385
|
+
if hasattr(provider, "force_flush"):
|
|
386
|
+
provider.force_flush(timeout_millis=5000)
|
|
387
|
+
if hasattr(provider, "shutdown"):
|
|
388
|
+
provider.shutdown()
|
|
389
|
+
|
|
390
|
+
# Flush LoggerProvider (don't shutdown — it may be shared/external)
|
|
391
|
+
try:
|
|
392
|
+
from opentelemetry._logs import get_logger_provider
|
|
393
|
+
|
|
394
|
+
log_provider = get_logger_provider()
|
|
395
|
+
if hasattr(log_provider, "force_flush"):
|
|
396
|
+
log_provider.force_flush(timeout_millis=5000)
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
|
|
400
|
+
_initialized = False
|
|
401
|
+
_current_config = None
|
|
402
|
+
logger.info("Botanu SDK shutdown complete")
|
|
403
|
+
|
|
404
|
+
except Exception as exc:
|
|
405
|
+
logger.error("Error during Botanu SDK shutdown: %s", exc)
|