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 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
+ [![PyPI](https://img.shields.io/pypi/v/python-otelio.svg)](https://pypi.org/project/python-otelio/)
49
+ [![Python](https://img.shields.io/pypi/pyversions/python-otelio.svg)](https://pypi.org/project/python-otelio/)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.