ondemand-obs 0.1.0__tar.gz

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.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: ondemand-obs
3
+ Version: 0.1.0
4
+ Summary: OpenTelemetry observability layer for the Ondemand platform
5
+ Author-email: Ondemand AI <dev@ondemand-ai.com.br>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://ondemand-ai.com.br
8
+ Project-URL: Repository, https://github.com/Ondemand-AI/ondemand-obs
9
+ Keywords: opentelemetry,observability,tracing,ondemand
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: opentelemetry-api>=1.27
16
+ Requires-Dist: opentelemetry-sdk>=1.27
17
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27
18
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.48
19
+ Requires-Dist: opentelemetry-instrumentation-requests>=0.48
20
+ Provides-Extra: temporal
21
+ Requires-Dist: temporalio>=1.7.0; extra == "temporal"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
25
+
26
+ # ondemand-obs
27
+
28
+ OpenTelemetry observability layer for the [Ondemand AI](https://ondemand-ai.com.br) platform. Ships traces, metrics, and logs to HyperDX (or any OTLP-compatible backend).
29
+
30
+ ## Usage
31
+
32
+ Set `HYPERDX_API_KEY` in your environment. That's it — the `ondemand-ai[worker]` SDK calls `configure_observability()` automatically at worker startup.
33
+
34
+ ```bash
35
+ HYPERDX_API_KEY=your-key OTEL_SERVICE_NAME=my-robot python -m src.main
36
+ ```
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install ondemand-obs # base (traces, metrics, logs, httpx/requests auto-instrument)
42
+ pip install "ondemand-obs[temporal]" # + Temporal TracingInterceptor
43
+ ```
44
+
45
+ ## Environment variables
46
+
47
+ | Variable | Required | Default |
48
+ |---|---|---|
49
+ | `HYPERDX_API_KEY` | Yes (disables if absent) | — |
50
+ | `OTEL_SERVICE_NAME` | No | passed to `configure_observability()` |
51
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | No | `https://in-otel.hyperdx.io` |
@@ -0,0 +1,26 @@
1
+ # ondemand-obs
2
+
3
+ OpenTelemetry observability layer for the [Ondemand AI](https://ondemand-ai.com.br) platform. Ships traces, metrics, and logs to HyperDX (or any OTLP-compatible backend).
4
+
5
+ ## Usage
6
+
7
+ Set `HYPERDX_API_KEY` in your environment. That's it — the `ondemand-ai[worker]` SDK calls `configure_observability()` automatically at worker startup.
8
+
9
+ ```bash
10
+ HYPERDX_API_KEY=your-key OTEL_SERVICE_NAME=my-robot python -m src.main
11
+ ```
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install ondemand-obs # base (traces, metrics, logs, httpx/requests auto-instrument)
17
+ pip install "ondemand-obs[temporal]" # + Temporal TracingInterceptor
18
+ ```
19
+
20
+ ## Environment variables
21
+
22
+ | Variable | Required | Default |
23
+ |---|---|---|
24
+ | `HYPERDX_API_KEY` | Yes (disables if absent) | — |
25
+ | `OTEL_SERVICE_NAME` | No | passed to `configure_observability()` |
26
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | No | `https://in-otel.hyperdx.io` |
@@ -0,0 +1,73 @@
1
+ """
2
+ ondemand-obs — OpenTelemetry observability for the Ondemand platform.
3
+
4
+ Usage in workers (automatic via ondemand-python SDK — no direct call needed):
5
+
6
+ from ondemand_obs import configure_observability
7
+ configure_observability("my-robot")
8
+
9
+ Set HYPERDX_API_KEY in env to enable. No-op if the key is absent.
10
+ """
11
+
12
+ import logging
13
+ import os
14
+ import threading
15
+
16
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME
17
+
18
+ from .config import ObsConfig
19
+ from .traces import setup_traces
20
+ from .metrics import setup_metrics
21
+ from .logs import setup_logs
22
+ from .instruments import setup_instruments
23
+
24
+ logger = logging.getLogger("ondemand_obs")
25
+
26
+ _configured = False
27
+ _lock = threading.Lock()
28
+ _log_handler = None
29
+
30
+
31
+ def configure_observability(service_name: str, config: ObsConfig | None = None) -> None:
32
+ global _configured, _log_handler
33
+
34
+ if config is None:
35
+ config = ObsConfig()
36
+
37
+ if not config.api_key:
38
+ return
39
+
40
+ with _lock:
41
+ if _configured:
42
+ return
43
+
44
+ resource = Resource.create({
45
+ SERVICE_NAME: os.environ.get("OTEL_SERVICE_NAME", service_name),
46
+ })
47
+
48
+ headers = {"authorization": config.api_key}
49
+
50
+ setup_traces(config.endpoint, headers, resource)
51
+ setup_metrics(config.endpoint, headers, resource)
52
+ _, handler = setup_logs(config.endpoint, headers, resource)
53
+ setup_instruments()
54
+
55
+ _log_handler = handler
56
+ _configured = True
57
+
58
+ logger.info(f"ondemand-obs: observability enabled for service '{service_name}'")
59
+
60
+
61
+ def is_configured() -> bool:
62
+ return _configured
63
+
64
+
65
+ def get_otlp_log_handler() -> logging.Handler | None:
66
+ """Return the OTLP LoggingHandler, or None if not configured."""
67
+ return _log_handler
68
+
69
+
70
+ def get_temporal_tracing_interceptor():
71
+ """Return a Temporal TracingInterceptor. Requires ondemand-obs[temporal]."""
72
+ from .temporal import get_tracing_interceptor
73
+ return get_tracing_interceptor()
@@ -0,0 +1,12 @@
1
+ import os
2
+ from dataclasses import dataclass, field
3
+
4
+ HYPERDX_OTLP_ENDPOINT = "https://in-otel.hyperdx.io"
5
+
6
+
7
+ @dataclass
8
+ class ObsConfig:
9
+ endpoint: str = field(default_factory=lambda: os.environ.get(
10
+ "OTEL_EXPORTER_OTLP_ENDPOINT", HYPERDX_OTLP_ENDPOINT
11
+ ))
12
+ api_key: str = field(default_factory=lambda: os.environ.get("HYPERDX_API_KEY", ""))
@@ -0,0 +1,13 @@
1
+ def setup_instruments() -> None:
2
+ """Register auto-instrumentation for common HTTP clients."""
3
+ try:
4
+ from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
5
+ HTTPXClientInstrumentor().instrument()
6
+ except ImportError:
7
+ pass
8
+
9
+ try:
10
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
11
+ RequestsInstrumentor().instrument()
12
+ except ImportError:
13
+ pass
@@ -0,0 +1,23 @@
1
+ import logging
2
+
3
+ from opentelemetry._logs import set_logger_provider
4
+ from opentelemetry.sdk._logs import LoggerProvider
5
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
6
+ from opentelemetry.sdk.resources import Resource
7
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
8
+
9
+ # LoggingHandler bridges Python's logging module → OTel log records → OTLP exporter
10
+ try:
11
+ from opentelemetry.sdk._logs import LoggingHandler
12
+ except ImportError:
13
+ # opentelemetry-sdk >= 1.28 may move this; fall back gracefully
14
+ from opentelemetry.sdk.logs import LoggingHandler # type: ignore[no-redef]
15
+
16
+
17
+ def setup_logs(endpoint: str, headers: dict, resource: Resource) -> tuple:
18
+ exporter = OTLPLogExporter(endpoint=f"{endpoint}/v1/logs", headers=headers)
19
+ provider = LoggerProvider(resource=resource)
20
+ provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
21
+ set_logger_provider(provider)
22
+ handler = LoggingHandler(level=logging.NOTSET, logger_provider=provider)
23
+ return provider, handler
@@ -0,0 +1,13 @@
1
+ from opentelemetry import metrics
2
+ from opentelemetry.sdk.metrics import MeterProvider
3
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
4
+ from opentelemetry.sdk.resources import Resource
5
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
6
+
7
+
8
+ def setup_metrics(endpoint: str, headers: dict, resource: Resource) -> MeterProvider:
9
+ exporter = OTLPMetricExporter(endpoint=f"{endpoint}/v1/metrics", headers=headers)
10
+ reader = PeriodicExportingMetricReader(exporter, export_interval_millis=60_000)
11
+ provider = MeterProvider(resource=resource, metric_readers=[reader])
12
+ metrics.set_meter_provider(provider)
13
+ return provider
@@ -0,0 +1,13 @@
1
+ def get_tracing_interceptor():
2
+ """
3
+ Return a Temporal TracingInterceptor wired to the active OTel tracer provider.
4
+ Requires ondemand-obs[temporal] (temporalio>=1.7).
5
+ """
6
+ try:
7
+ from temporalio.contrib.opentelemetry import TracingInterceptor
8
+ except ImportError as e:
9
+ raise ImportError(
10
+ "temporalio is required for Temporal tracing. "
11
+ "Install ondemand-obs[temporal] or ondemand-ai[worker]."
12
+ ) from e
13
+ return TracingInterceptor()
@@ -0,0 +1,13 @@
1
+ from opentelemetry import trace
2
+ from opentelemetry.sdk.resources import Resource
3
+ from opentelemetry.sdk.trace import TracerProvider
4
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
5
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
6
+
7
+
8
+ def setup_traces(endpoint: str, headers: dict, resource: Resource) -> TracerProvider:
9
+ exporter = OTLPSpanExporter(endpoint=f"{endpoint}/v1/traces", headers=headers)
10
+ provider = TracerProvider(resource=resource)
11
+ provider.add_span_processor(BatchSpanProcessor(exporter))
12
+ trace.set_tracer_provider(provider)
13
+ return provider
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: ondemand-obs
3
+ Version: 0.1.0
4
+ Summary: OpenTelemetry observability layer for the Ondemand platform
5
+ Author-email: Ondemand AI <dev@ondemand-ai.com.br>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://ondemand-ai.com.br
8
+ Project-URL: Repository, https://github.com/Ondemand-AI/ondemand-obs
9
+ Keywords: opentelemetry,observability,tracing,ondemand
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: opentelemetry-api>=1.27
16
+ Requires-Dist: opentelemetry-sdk>=1.27
17
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27
18
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.48
19
+ Requires-Dist: opentelemetry-instrumentation-requests>=0.48
20
+ Provides-Extra: temporal
21
+ Requires-Dist: temporalio>=1.7.0; extra == "temporal"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
25
+
26
+ # ondemand-obs
27
+
28
+ OpenTelemetry observability layer for the [Ondemand AI](https://ondemand-ai.com.br) platform. Ships traces, metrics, and logs to HyperDX (or any OTLP-compatible backend).
29
+
30
+ ## Usage
31
+
32
+ Set `HYPERDX_API_KEY` in your environment. That's it — the `ondemand-ai[worker]` SDK calls `configure_observability()` automatically at worker startup.
33
+
34
+ ```bash
35
+ HYPERDX_API_KEY=your-key OTEL_SERVICE_NAME=my-robot python -m src.main
36
+ ```
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install ondemand-obs # base (traces, metrics, logs, httpx/requests auto-instrument)
42
+ pip install "ondemand-obs[temporal]" # + Temporal TracingInterceptor
43
+ ```
44
+
45
+ ## Environment variables
46
+
47
+ | Variable | Required | Default |
48
+ |---|---|---|
49
+ | `HYPERDX_API_KEY` | Yes (disables if absent) | — |
50
+ | `OTEL_SERVICE_NAME` | No | passed to `configure_observability()` |
51
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | No | `https://in-otel.hyperdx.io` |
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ ondemand_obs/__init__.py
4
+ ondemand_obs/config.py
5
+ ondemand_obs/instruments.py
6
+ ondemand_obs/logs.py
7
+ ondemand_obs/metrics.py
8
+ ondemand_obs/temporal.py
9
+ ondemand_obs/traces.py
10
+ ondemand_obs.egg-info/PKG-INFO
11
+ ondemand_obs.egg-info/SOURCES.txt
12
+ ondemand_obs.egg-info/dependency_links.txt
13
+ ondemand_obs.egg-info/requires.txt
14
+ ondemand_obs.egg-info/top_level.txt
15
+ tests/test_configure.py
@@ -0,0 +1,12 @@
1
+ opentelemetry-api>=1.27
2
+ opentelemetry-sdk>=1.27
3
+ opentelemetry-exporter-otlp-proto-http>=1.27
4
+ opentelemetry-instrumentation-httpx>=0.48
5
+ opentelemetry-instrumentation-requests>=0.48
6
+
7
+ [dev]
8
+ pytest>=8.0
9
+ pytest-asyncio>=0.23
10
+
11
+ [temporal]
12
+ temporalio>=1.7.0
@@ -0,0 +1 @@
1
+ ondemand_obs
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ondemand-obs"
7
+ version = "0.1.0"
8
+ description = "OpenTelemetry observability layer for the Ondemand platform"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [
13
+ {name = "Ondemand AI", email = "dev@ondemand-ai.com.br"}
14
+ ]
15
+ keywords = ["opentelemetry", "observability", "tracing", "ondemand"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+
22
+ dependencies = [
23
+ "opentelemetry-api>=1.27",
24
+ "opentelemetry-sdk>=1.27",
25
+ "opentelemetry-exporter-otlp-proto-http>=1.27",
26
+ "opentelemetry-instrumentation-httpx>=0.48",
27
+ "opentelemetry-instrumentation-requests>=0.48",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ temporal = ["temporalio>=1.7.0"]
32
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://ondemand-ai.com.br"
36
+ Repository = "https://github.com/Ondemand-AI/ondemand-obs"
37
+
38
+ [tool.setuptools.packages.find]
39
+ include = ["ondemand_obs*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ import importlib
2
+ import os
3
+ import sys
4
+
5
+
6
+ def _reload_module():
7
+ """Reload ondemand_obs to reset global state between tests."""
8
+ for mod in list(sys.modules.keys()):
9
+ if mod.startswith("ondemand_obs"):
10
+ del sys.modules[mod]
11
+ import ondemand_obs
12
+ return ondemand_obs
13
+
14
+
15
+ def test_noop_without_api_key(monkeypatch):
16
+ monkeypatch.delenv("HYPERDX_API_KEY", raising=False)
17
+ obs = _reload_module()
18
+ obs.configure_observability("test-service")
19
+ assert not obs.is_configured()
20
+ assert obs.get_otlp_log_handler() is None
21
+
22
+
23
+ def test_idempotent(monkeypatch):
24
+ monkeypatch.setenv("HYPERDX_API_KEY", "test-key-123")
25
+ obs = _reload_module()
26
+ obs.configure_observability("svc-a")
27
+ obs.configure_observability("svc-b") # second call should be no-op
28
+ assert obs.is_configured()
29
+ # If second call ran, it would raise because providers are already set.
30
+ # Reaching here means idempotency guard worked.
31
+
32
+
33
+ def test_configured_with_key(monkeypatch):
34
+ monkeypatch.setenv("HYPERDX_API_KEY", "test-key-456")
35
+ obs = _reload_module()
36
+ obs.configure_observability("my-robot")
37
+ assert obs.is_configured()
38
+ assert obs.get_otlp_log_handler() is not None