python-otelio 0.0.1__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.
- otelio/__init__.py +32 -0
- otelio/bootstrap.py +65 -0
- otelio/config.py +32 -0
- otelio/exporters.py +41 -0
- otelio/helpers.py +84 -0
- otelio/logging.py +97 -0
- otelio/tracing.py +46 -0
- python_otelio-0.0.1.dist-info/METADATA +161 -0
- python_otelio-0.0.1.dist-info/RECORD +11 -0
- python_otelio-0.0.1.dist-info/WHEEL +4 -0
- python_otelio-0.0.1.dist-info/licenses/LICENSE +21 -0
otelio/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Otelio — a small OpenTelemetry + Loguru toolkit for Python services.
|
|
3
|
+
|
|
4
|
+
Import surface kept intentionally small: bootstrap once with ``init_otelio``,
|
|
5
|
+
then use ``otel_span`` / helpers anywhere in the codebase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .bootstrap import init_otelio
|
|
9
|
+
from .helpers import (
|
|
10
|
+
otel_add_event,
|
|
11
|
+
otel_context_from_headers,
|
|
12
|
+
otel_get_all_baggage,
|
|
13
|
+
otel_get_baggage,
|
|
14
|
+
otel_inject_headers,
|
|
15
|
+
otel_set_attributes,
|
|
16
|
+
otel_set_baggage,
|
|
17
|
+
)
|
|
18
|
+
from .tracing import otel_current_span, otel_get_tracer, otel_span
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"init_otelio",
|
|
22
|
+
"otel_add_event",
|
|
23
|
+
"otel_context_from_headers",
|
|
24
|
+
"otel_current_span",
|
|
25
|
+
"otel_get_all_baggage",
|
|
26
|
+
"otel_get_baggage",
|
|
27
|
+
"otel_get_tracer",
|
|
28
|
+
"otel_inject_headers",
|
|
29
|
+
"otel_set_attributes",
|
|
30
|
+
"otel_set_baggage",
|
|
31
|
+
"otel_span",
|
|
32
|
+
]
|
otelio/bootstrap.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""One-call wiring: providers, processors, the Loguru bridge, and shutdown."""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
from opentelemetry._logs import set_logger_provider
|
|
10
|
+
from opentelemetry.sdk._logs import LoggerProvider
|
|
11
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
12
|
+
from opentelemetry.sdk.resources import Resource
|
|
13
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
14
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
15
|
+
|
|
16
|
+
from .config import Settings, load_settings
|
|
17
|
+
from .exporters import build_log_exporter, build_span_exporter
|
|
18
|
+
from .logging import setup_loguru
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def init_otelio(
|
|
22
|
+
service_name: str,
|
|
23
|
+
service_version: str,
|
|
24
|
+
environment: str | None = None,
|
|
25
|
+
resource_attributes: Mapping[str, Any] | None = None,
|
|
26
|
+
) -> Settings:
|
|
27
|
+
"""
|
|
28
|
+
Initialise tracing + logging once at process start; returns the resolved settings.
|
|
29
|
+
|
|
30
|
+
Pass ``resource_attributes`` to stamp extra resource-level attributes (e.g.
|
|
31
|
+
``service.namespace``, ``service.instance.id``, ``cloud.region``) onto every span
|
|
32
|
+
and log this process emits. The canonical ``service.name`` / ``service.version`` /
|
|
33
|
+
``deployment.environment`` keys always win, so they cannot be clobbered here.
|
|
34
|
+
|
|
35
|
+
Registers an :mod:`atexit` hook that flushes Loguru and shuts the providers down
|
|
36
|
+
so buffered spans/logs are exported on a clean exit.
|
|
37
|
+
"""
|
|
38
|
+
s = load_settings(service_name, service_version, environment)
|
|
39
|
+
|
|
40
|
+
resource = Resource.create(
|
|
41
|
+
{
|
|
42
|
+
**(resource_attributes or {}),
|
|
43
|
+
"service.name": s.service_name,
|
|
44
|
+
"service.version": s.service_version,
|
|
45
|
+
"deployment.environment": s.environment,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
50
|
+
tracer_provider.add_span_processor(BatchSpanProcessor(build_span_exporter(s)))
|
|
51
|
+
trace.set_tracer_provider(tracer_provider)
|
|
52
|
+
|
|
53
|
+
logger_provider = LoggerProvider(resource=resource)
|
|
54
|
+
logger_provider.add_log_record_processor(BatchLogRecordProcessor(build_log_exporter(s)))
|
|
55
|
+
set_logger_provider(logger_provider)
|
|
56
|
+
|
|
57
|
+
setup_loguru(logger_provider)
|
|
58
|
+
|
|
59
|
+
def _shutdown() -> None:
|
|
60
|
+
logger.complete()
|
|
61
|
+
tracer_provider.shutdown()
|
|
62
|
+
logger_provider.shutdown()
|
|
63
|
+
|
|
64
|
+
atexit.register(_shutdown)
|
|
65
|
+
return s
|
otelio/config.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Settings for otelio, resolved from environment variables with sane defaults."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Settings:
|
|
9
|
+
"""Resolved telemetry configuration for one service."""
|
|
10
|
+
|
|
11
|
+
service_name: str
|
|
12
|
+
service_version: str
|
|
13
|
+
environment: str
|
|
14
|
+
target: str # "otlp" | "azure"
|
|
15
|
+
otlp_endpoint: str
|
|
16
|
+
azure_conn_str: str | None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_settings(
|
|
20
|
+
service_name: str,
|
|
21
|
+
service_version: str,
|
|
22
|
+
environment: str | None = None,
|
|
23
|
+
) -> Settings:
|
|
24
|
+
"""Build :class:`Settings`, letting environment variables override the args."""
|
|
25
|
+
return Settings(
|
|
26
|
+
service_name=os.getenv("OTEL_SERVICE_NAME", service_name),
|
|
27
|
+
service_version=service_version,
|
|
28
|
+
environment=environment or os.getenv("DEPLOYMENT_ENVIRONMENT", "local"),
|
|
29
|
+
target=os.getenv("OTELIO_TARGET", "otlp").lower(), # local -> otlp/signoz
|
|
30
|
+
otlp_endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"),
|
|
31
|
+
azure_conn_str=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"),
|
|
32
|
+
)
|
otelio/exporters.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Span and log exporter factories.
|
|
3
|
+
|
|
4
|
+
The backend-specific SDKs are imported lazily so a project only needs the deps
|
|
5
|
+
for the target it actually uses (``otlp`` for SigNoz, ``azure`` for App Insights).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from opentelemetry.sdk._logs.export import LogRecordExporter
|
|
9
|
+
from opentelemetry.sdk.trace.export import SpanExporter
|
|
10
|
+
|
|
11
|
+
from .config import Settings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_span_exporter(s: Settings) -> SpanExporter:
|
|
15
|
+
"""Return the span exporter for the configured target."""
|
|
16
|
+
if s.target == "azure":
|
|
17
|
+
from azure.monitor.opentelemetry.exporter import ( # noqa: PLC0415 (optional dep)
|
|
18
|
+
AzureMonitorTraceExporter,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return AzureMonitorTraceExporter(connection_string=s.azure_conn_str)
|
|
22
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # noqa: PLC0415
|
|
23
|
+
OTLPSpanExporter,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return OTLPSpanExporter(endpoint=s.otlp_endpoint)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_log_exporter(s: Settings) -> LogRecordExporter:
|
|
30
|
+
"""Return the log-record exporter for the configured target."""
|
|
31
|
+
if s.target == "azure":
|
|
32
|
+
from azure.monitor.opentelemetry.exporter import ( # noqa: PLC0415 (optional dep)
|
|
33
|
+
AzureMonitorLogExporter,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return AzureMonitorLogExporter(connection_string=s.azure_conn_str)
|
|
37
|
+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # noqa: PLC0415
|
|
38
|
+
OTLPLogExporter,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return OTLPLogExporter(endpoint=s.otlp_endpoint)
|
otelio/helpers.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Context propagation and attribute/event helpers for working with spans."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from opentelemetry import baggage
|
|
7
|
+
from opentelemetry.context import Context, attach, get_current
|
|
8
|
+
from opentelemetry.propagate import extract, inject
|
|
9
|
+
from opentelemetry.trace import Span
|
|
10
|
+
|
|
11
|
+
from .tracing import otel_current_span
|
|
12
|
+
|
|
13
|
+
# ---- propagation (W3C traceparent + baggage) ----
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def otel_inject_headers(headers: dict[str, str] | None = None) -> dict[str, str]:
|
|
17
|
+
"""Inject the current trace context into ``headers`` (adds ``traceparent``)."""
|
|
18
|
+
headers = headers if headers is not None else {}
|
|
19
|
+
inject(headers)
|
|
20
|
+
return headers
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def otel_context_from_headers(headers: Mapping[str, str]) -> Context:
|
|
24
|
+
"""Extract a trace context from inbound ``headers``; pass to ``span(context=...)``."""
|
|
25
|
+
return extract(headers)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---- baggage (cross-service key/values, ride the `baggage` header) ----
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def otel_set_baggage(items: Mapping[str, str]) -> object:
|
|
32
|
+
"""
|
|
33
|
+
Put key/value pairs into baggage so they propagate to every downstream hop.
|
|
34
|
+
|
|
35
|
+
Mirrors :func:`otel_set_attributes` — pass a mapping object. Unlike span
|
|
36
|
+
attributes (local to one service), baggage rides the W3C ``baggage`` header
|
|
37
|
+
through every downstream hop, so it is ideal for cross-cutting IDs such as
|
|
38
|
+
``tenant.id`` / ``request.id`` / ``user.id``.
|
|
39
|
+
|
|
40
|
+
Baggage is sent in plaintext to every downstream service — **never put secrets
|
|
41
|
+
or PII in it**, and keep entries small (it is header weight on every call).
|
|
42
|
+
|
|
43
|
+
Baggage does not become span attributes automatically; copy what you want onto
|
|
44
|
+
a span with :func:`otel_set_attributes` (e.g. via :func:`otel_get_all_baggage`).
|
|
45
|
+
|
|
46
|
+
Attaches the updated context as current and returns a detach token. In a
|
|
47
|
+
request-scoped flow (e.g. FastAPI middleware) keep the token and call
|
|
48
|
+
``opentelemetry.context.detach(token)`` when the request ends so baggage does
|
|
49
|
+
not leak into the next request handled on the same context.
|
|
50
|
+
"""
|
|
51
|
+
ctx = get_current()
|
|
52
|
+
for key, value in items.items():
|
|
53
|
+
ctx = baggage.set_baggage(key, value, context=ctx)
|
|
54
|
+
return attach(ctx)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def otel_get_baggage(key: str) -> str | None:
|
|
58
|
+
"""Return a single baggage value from the current context, or ``None``."""
|
|
59
|
+
value = baggage.get_baggage(key)
|
|
60
|
+
return None if value is None else str(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def otel_get_all_baggage() -> dict[str, str]:
|
|
64
|
+
"""Return all baggage entries in the current context as a plain dict."""
|
|
65
|
+
return {key: str(value) for key, value in baggage.get_all().items()}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---- attributes / events ----
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def otel_set_attributes(attributes: Mapping[str, Any], span: Span | None = None) -> None:
|
|
72
|
+
"""Set attributes on the current span (or ``span``) when it is recording."""
|
|
73
|
+
span = span or otel_current_span()
|
|
74
|
+
if span and span.is_recording():
|
|
75
|
+
span.set_attributes(dict(attributes))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def otel_add_event(
|
|
79
|
+
name: str, attributes: Mapping[str, Any] | None = None, span: Span | None = None
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Add a timestamped event to the current span (or ``span``) when recording."""
|
|
82
|
+
span = span or otel_current_span()
|
|
83
|
+
if span and span.is_recording():
|
|
84
|
+
span.add_event(name, attributes=dict(attributes or {}))
|
otelio/logging.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge Loguru into the OpenTelemetry logs pipeline.
|
|
3
|
+
|
|
4
|
+
Loguru stays the single logging API for the app; every record is mirrored to an
|
|
5
|
+
OTel ``LoggingHandler`` (so logs are exported and correlated to the active span)
|
|
6
|
+
while a patcher stamps ``trace_id`` / ``span_id`` onto the console line.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from opentelemetry import trace
|
|
15
|
+
from opentelemetry.context import attach, detach
|
|
16
|
+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
17
|
+
from opentelemetry.trace import (
|
|
18
|
+
NonRecordingSpan,
|
|
19
|
+
SpanContext,
|
|
20
|
+
TraceFlags,
|
|
21
|
+
set_span_in_context,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from loguru import Message
|
|
26
|
+
|
|
27
|
+
_LOGURU_TO_STD = {
|
|
28
|
+
"TRACE": 5,
|
|
29
|
+
"DEBUG": 10,
|
|
30
|
+
"INFO": 20,
|
|
31
|
+
"SUCCESS": 25,
|
|
32
|
+
"WARNING": 30,
|
|
33
|
+
"ERROR": 40,
|
|
34
|
+
"CRITICAL": 50,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _trace_patcher(record: dict) -> None:
|
|
39
|
+
ctx = trace.get_current_span().get_span_context()
|
|
40
|
+
if ctx and ctx.trace_id:
|
|
41
|
+
record["extra"]["trace_id"] = format(ctx.trace_id, "032x")
|
|
42
|
+
record["extra"]["span_id"] = format(ctx.span_id, "016x")
|
|
43
|
+
else:
|
|
44
|
+
record["extra"]["trace_id"] = "-"
|
|
45
|
+
record["extra"]["span_id"] = "-"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def setup_loguru(
|
|
49
|
+
logger_provider: LoggerProvider,
|
|
50
|
+
console_level: str = "INFO",
|
|
51
|
+
export_level: str = "DEBUG",
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Reconfigure Loguru with a console sink and an OTel-export sink."""
|
|
54
|
+
otel_handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
|
|
55
|
+
|
|
56
|
+
def _otel_sink(message: "Message") -> None:
|
|
57
|
+
r = message.record
|
|
58
|
+
std = logging.LogRecord(
|
|
59
|
+
name=r["name"] or "otelio",
|
|
60
|
+
level=_LOGURU_TO_STD.get(r["level"].name, 20),
|
|
61
|
+
pathname=r["file"].path,
|
|
62
|
+
lineno=r["line"],
|
|
63
|
+
msg=r["message"],
|
|
64
|
+
args=(),
|
|
65
|
+
exc_info=r["exception"], # loguru's (type, value, tb) namedtuple
|
|
66
|
+
func=r["function"],
|
|
67
|
+
)
|
|
68
|
+
# enqueue=True runs this on Loguru's writer thread, where the OTel
|
|
69
|
+
# contextvar is empty — so re-attach the span context the patcher
|
|
70
|
+
# captured (on the originating thread) before emitting, otherwise the
|
|
71
|
+
# exported record loses its trace_id/span_id.
|
|
72
|
+
tid = r["extra"].get("trace_id")
|
|
73
|
+
sid = r["extra"].get("span_id")
|
|
74
|
+
token = None
|
|
75
|
+
if tid and tid != "-":
|
|
76
|
+
span_ctx = SpanContext(
|
|
77
|
+
trace_id=int(tid, 16),
|
|
78
|
+
span_id=int(sid, 16),
|
|
79
|
+
is_remote=False,
|
|
80
|
+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
81
|
+
)
|
|
82
|
+
token = attach(set_span_in_context(NonRecordingSpan(span_ctx)))
|
|
83
|
+
try:
|
|
84
|
+
otel_handler.emit(std) # backend; span context attached here
|
|
85
|
+
finally:
|
|
86
|
+
if token is not None:
|
|
87
|
+
detach(token)
|
|
88
|
+
|
|
89
|
+
logger.remove() # drop loguru's default stderr sink
|
|
90
|
+
logger.configure(patcher=_trace_patcher)
|
|
91
|
+
logger.add(
|
|
92
|
+
sys.stderr,
|
|
93
|
+
level=console_level,
|
|
94
|
+
format="<green>{time:HH:mm:ss.SSS}</green> | {level: <8} | "
|
|
95
|
+
"trace={extra[trace_id]} | {name}:{line} - {message}",
|
|
96
|
+
)
|
|
97
|
+
logger.add(_otel_sink, level=export_level, enqueue=True)
|
otelio/tracing.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Span creation: a ``span()`` context manager plus low-level span access."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator, Mapping
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
from opentelemetry.context import Context
|
|
9
|
+
from opentelemetry.trace import Span, SpanKind, Status, StatusCode, Tracer
|
|
10
|
+
|
|
11
|
+
_TRACER_NAME = "otelio"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def otel_get_tracer() -> Tracer:
|
|
15
|
+
"""Return the shared otelio tracer."""
|
|
16
|
+
return trace.get_tracer(_TRACER_NAME)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def otel_current_span() -> Span:
|
|
20
|
+
"""Return the span active in the current context (a no-op span if none)."""
|
|
21
|
+
return trace.get_current_span()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def otel_span(
|
|
26
|
+
name: str,
|
|
27
|
+
attributes: Mapping[str, Any] | None = None,
|
|
28
|
+
kind: SpanKind = SpanKind.INTERNAL,
|
|
29
|
+
context: Context | None = None,
|
|
30
|
+
) -> Iterator[Span]:
|
|
31
|
+
"""
|
|
32
|
+
Start ``name`` as the current span; records and re-raises any exception.
|
|
33
|
+
|
|
34
|
+
Pass ``context`` (from :func:`otel_context_from_headers`) to continue an
|
|
35
|
+
inbound distributed trace.
|
|
36
|
+
"""
|
|
37
|
+
tracer = otel_get_tracer()
|
|
38
|
+
with tracer.start_as_current_span(name, context=context, kind=kind) as s:
|
|
39
|
+
if attributes:
|
|
40
|
+
s.set_attributes(dict(attributes))
|
|
41
|
+
try:
|
|
42
|
+
yield s
|
|
43
|
+
except Exception as e:
|
|
44
|
+
s.record_exception(e)
|
|
45
|
+
s.set_status(Status(StatusCode.ERROR, str(e)))
|
|
46
|
+
raise
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-otelio
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Otelio: OpenTelemetry + Loguru toolkit for Python services
|
|
5
|
+
Project-URL: Homepage, https://github.com/code4mk/python-otelio
|
|
6
|
+
Project-URL: Source, https://github.com/code4mk/python-otelio
|
|
7
|
+
Project-URL: Changelog, https://github.com/code4mk/python-otelio/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Documentation, https://github.com/code4mk/python-otelio
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/code4mk/python-otelio/issues
|
|
10
|
+
Author-email: Mostafa Kamal <hiremostafa@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: code4mk,logging,loguru,observability,opentelemetry,otel,python,tracing
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: loguru>=0.7.3
|
|
25
|
+
Requires-Dist: opentelemetry-api==1.40.*
|
|
26
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.40.*
|
|
27
|
+
Requires-Dist: opentelemetry-sdk==1.40.*
|
|
28
|
+
Provides-Extra: azure
|
|
29
|
+
Requires-Dist: azure-monitor-opentelemetry-exporter>=1.0.0b53; extra == 'azure'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.0.270; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# otelio
|
|
39
|
+
|
|
40
|
+
> Python OpenTelemetry + Loguru toolkit
|
|
41
|
+
|
|
42
|
+
A small, batteries-included **OpenTelemetry + [Loguru](https://github.com/Delgan/loguru)**
|
|
43
|
+
toolkit for Python services. Call `init_otelio(...)` once at startup and you get **traces**
|
|
44
|
+
and **logs** that are automatically correlated by `trace_id` / `span_id`, exported over
|
|
45
|
+
**OTLP/gRPC** (SigNoz, Grafana, Jaeger, any OTLP collector) or to **Azure Application
|
|
46
|
+
Insights** — switchable with a single environment variable, no code changes.
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/python-otelio/)
|
|
49
|
+
[](https://pypi.org/project/python-otelio/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- **One call to wire everything** — `init_otelio(...)` sets up the tracer + logger
|
|
57
|
+
providers, the Loguru bridge, and a clean-shutdown flush hook.
|
|
58
|
+
- **Logs correlate to spans automatically** — keep using Loguru; every record is stamped
|
|
59
|
+
with the active `trace_id` / `span_id` and exported.
|
|
60
|
+
- **Backend-agnostic** — OTLP/gRPC or Azure App Insights via the `OTELIO_TARGET` env var.
|
|
61
|
+
Exporter SDKs are imported lazily, so you only install what you use.
|
|
62
|
+
- **Cross-service tracing built in** — W3C `traceparent` + `baggage` propagation helpers so
|
|
63
|
+
one request shows up as a single connected trace across service boundaries.
|
|
64
|
+
- **Tiny surface** — eleven well-documented functions, nothing to configure in code.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install python-otelio # core + OTLP/gRPC exporter
|
|
70
|
+
pip install "python-otelio[azure]" # also the Azure Application Insights exporter
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Python 3.10+. The distribution is named **`python-otelio`** on PyPI but imports
|
|
74
|
+
as **`otelio`** (`from otelio import ...`).
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from otelio import init_otelio, otel_span, otel_set_attributes
|
|
80
|
+
from loguru import logger
|
|
81
|
+
|
|
82
|
+
# 1. Bootstrap once, at process start (before anything emits telemetry).
|
|
83
|
+
init_otelio(service_name="my-service", service_version="1.0.0")
|
|
84
|
+
|
|
85
|
+
# 2. Log with Loguru as usual — records are stamped with the active span.
|
|
86
|
+
logger.info("service started")
|
|
87
|
+
|
|
88
|
+
# 3. Wrap units of work in spans; exceptions are recorded and re-raised.
|
|
89
|
+
with otel_span("handle_request", attributes={"route": "/search"}):
|
|
90
|
+
otel_set_attributes({"result.count": 12})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
All configuration is via environment variables.
|
|
96
|
+
|
|
97
|
+
| Variable | Default | Meaning |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `OTELIO_TARGET` | `otlp` | `otlp` (any OTLP/gRPC collector) or `azure` (App Insights). |
|
|
100
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | OTLP/gRPC collector endpoint (target `otlp`). |
|
|
101
|
+
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | — | App Insights connection string (target `azure`). |
|
|
102
|
+
| `OTEL_SERVICE_NAME` | the `service_name` arg | Overrides the service name. |
|
|
103
|
+
| `DEPLOYMENT_ENVIRONMENT` | `local` | Set as the `deployment.environment` resource attribute. |
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# OTLP collector (SigNoz, Grafana, Jaeger, ...)
|
|
107
|
+
export OTELIO_TARGET=otlp
|
|
108
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
|
109
|
+
|
|
110
|
+
# Azure Application Insights
|
|
111
|
+
export OTELIO_TARGET=azure
|
|
112
|
+
export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=...;IngestionEndpoint=..."
|
|
113
|
+
export DEPLOYMENT_ENVIRONMENT=production
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Public API (`from otelio import ...`)
|
|
117
|
+
|
|
118
|
+
| Symbol | Purpose |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `init_otelio(service_name, service_version, environment=None, resource_attributes=None)` | Bootstrap tracing + logging once at startup. `resource_attributes` adds extra resource-level keys to every span + log. Returns the resolved `Settings`. |
|
|
121
|
+
| `otel_span(name, attributes=None, kind=SpanKind.INTERNAL, context=None)` | Context manager that starts a span, records exceptions, and re-raises. |
|
|
122
|
+
| `otel_current_span()` | The span active in the current context. |
|
|
123
|
+
| `otel_get_tracer()` | The shared `otelio` tracer. |
|
|
124
|
+
| `otel_inject_headers(headers=None)` | Inject the current trace context + baggage into an outbound header dict. |
|
|
125
|
+
| `otel_context_from_headers(headers)` | Extract a trace context (+ baggage) from inbound headers; pass to `otel_span(context=...)`. |
|
|
126
|
+
| `otel_set_baggage(items)` | Put a mapping of key/values into baggage so they propagate downstream. Returns a detach token. |
|
|
127
|
+
| `otel_get_baggage(key)` | Read one baggage value from the current context (or `None`). |
|
|
128
|
+
| `otel_get_all_baggage()` | Read all baggage entries as a plain `dict`. |
|
|
129
|
+
| `otel_set_attributes(attributes, span=None)` | Set attributes on the current span, or `span` if given (guards `is_recording()`). |
|
|
130
|
+
| `otel_add_event(name, attributes=None, span=None)` | Add a timestamped event to the current span, or `span` if given. |
|
|
131
|
+
|
|
132
|
+
## Context propagation across services
|
|
133
|
+
|
|
134
|
+
`otelio` carries the W3C `traceparent` + `baggage` headers automatically, so one request
|
|
135
|
+
shows up as a single connected trace across service boundaries:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
import httpx
|
|
139
|
+
from opentelemetry.trace import SpanKind
|
|
140
|
+
from otelio import otel_inject_headers, otel_context_from_headers, otel_span
|
|
141
|
+
|
|
142
|
+
# Outbound — inject context into the request headers
|
|
143
|
+
with otel_span("call_downstream", kind=SpanKind.CLIENT):
|
|
144
|
+
headers = otel_inject_headers({"Authorization": token})
|
|
145
|
+
resp = httpx.post(url, headers=headers, json=payload)
|
|
146
|
+
|
|
147
|
+
# Inbound — continue the caller's trace
|
|
148
|
+
ctx = otel_context_from_headers(request.headers)
|
|
149
|
+
with otel_span("serve_request", kind=SpanKind.SERVER, context=ctx):
|
|
150
|
+
...
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
See the full [usage guide](https://github.com/code4mk/python-otelio/blob/main/docs/usage.md)
|
|
156
|
+
for bootstrapping, spans, correlated logging, context propagation, baggage, and a complete
|
|
157
|
+
FastAPI example.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
[MIT](LICENSE) © code4mk
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
otelio/__init__.py,sha256=kNm1onGialyL4bf_gkhBGhErtnHRQGRfvBYClNkRsv8,800
|
|
2
|
+
otelio/bootstrap.py,sha256=PZbP9cI9xFcthASoBCDqSTi1Z3JUFeGg-fFjXayZhvU,2344
|
|
3
|
+
otelio/config.py,sha256=OxcKId-8CaONpc5HgrwzJjxWhe_2WPhX4GfsT9teWPc,1057
|
|
4
|
+
otelio/exporters.py,sha256=JWheVBFfhhjJWWKAlotzG4MSuSlLECR6chV421g3cuw,1435
|
|
5
|
+
otelio/helpers.py,sha256=fNciSTwdcZMNA8G1ZPVbV-ncuoDnE3KkI4qKmuYOmG8,3248
|
|
6
|
+
otelio/logging.py,sha256=-aNmlt0cJLVYD9Z5e8XKim42MjdWiRYudKhdzQdAOhQ,3184
|
|
7
|
+
otelio/tracing.py,sha256=bMwUvByAYdUyOBUlF9Z5LKIHjx3j_cinPQz_QP2JDDE,1382
|
|
8
|
+
python_otelio-0.0.1.dist-info/METADATA,sha256=TsaSfRUFbAptWoHu0y5bnpRIEvt8D-p3Pih1sfd-pRc,7414
|
|
9
|
+
python_otelio-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
10
|
+
python_otelio-0.0.1.dist-info/licenses/LICENSE,sha256=VJxb9K5BrmXV5gu6vuCc2NmzPQNq-PLJW32BjUD26pE,1064
|
|
11
|
+
python_otelio-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 code4mk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|