elinaobs 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.
elinaobs-0.1.0/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ElinaObs Contributors
4
+
5
+ Third-party files retain their original copyright notices and licenses.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: elinaobs
3
+ Version: 0.1.0
4
+ Summary: Framework-agnostic Python LLM tracing using OpenTelemetry
5
+ Author: ElinaObs Contributors
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: opentelemetry-api<2,>=1.36
11
+ Requires-Dist: opentelemetry-sdk<2,>=1.36
12
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.36
13
+ Requires-Dist: requests<3,>=2
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8; extra == "dev"
16
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
17
+ Requires-Dist: mypy>=1.10; extra == "dev"
18
+ Requires-Dist: opentelemetry-instrumentation-requests<1,>=0.57b0; extra == "dev"
19
+ Dynamic: license-file
20
+
21
+ # ElinaObs Python SDK
22
+
23
+ Python tracing SDK for self-hosted Elina. Licensed under MIT.
24
+
25
+ ## Install
26
+
27
+ Once published on PyPI:
28
+
29
+ ```bash
30
+ python -m pip install elinaobs
31
+ ```
32
+
33
+ For development from this repository:
34
+
35
+ ```bash
36
+ python -m pip install -e '.[dev]'
37
+ pytest -q
38
+ ```
39
+
40
+ ## Public API
41
+
42
+ ```python
43
+ from elinaobs import init, trace, flush, shutdown, diagnose, submit_feedback
44
+
45
+ init(api_key="...", endpoint="http://localhost:3000/v1/traces", service_name="my-agent")
46
+
47
+ @trace(name="Agent", as_type="agent")
48
+ async def run(question):
49
+ return await your_code(question)
50
+
51
+ # The existing OTel-instrumented operations called inside run() are children.
52
+ ```
53
+
54
+ Application feedback is optional. After a trace has finished, an app with a verdict can send it using the same project key. Inside a decorated function, capture the trace ID and keep it with the result:
55
+
56
+ ```python
57
+ from opentelemetry import trace as otel_trace
58
+ trace_id = f"{otel_trace.get_current_span().get_span_context().trace_id:032x}"
59
+ # Later, after the traced function returns:
60
+ submit_feedback(trace_id, "bad", note="Wrong answer") # trace_id is 32 lowercase hex characters
61
+ ```
62
+
63
+ Apps without a feedback signal do not call this function. Feedback is stored separately from the dashboard's human review. Keep the project key in server-side code, not in a browser.
64
+
65
+ To send a demo trace to the local Elina server, create a project in the dashboard and copy its one-time ingestion key. Then, from this directory in PowerShell:
66
+
67
+ ```powershell
68
+ $env:ELINA_WRITE_KEY = "paste-project-ingestion-key"
69
+ $env:ELINA_BASE_URL = "http://127.0.0.1:3000"
70
+ python examples/agent.py
71
+ ```
72
+
73
+ The script prints the trace ID and reports whether export succeeded. Open the project’s Traces page and select that ID to inspect the three spans and their captured input/output.
74
+
75
+ Only operations that are explicitly decorated or already instrumented by a compatible OpenTelemetry instrumentation package create spans. **No framework integrations are installed, imported, or configured by ElinaObs.**
76
+
77
+ The Elina receiver lives in the sibling `elina-server` directory. Start it before running `examples/agent.py`. The default tests use in-memory and local HTTP receivers.
78
+
79
+ To also verify export through a real OpenTelemetry Collector, set `OTELCOL_BINARY` to the Collector executable path and run `python -m pytest -q tests/test_collector.py`. This optional test starts a local Collector with a file exporter and checks the received spans.
80
+
81
+ ## What it does
82
+
83
+ - Sync + async function decorator with native OTel parent context.
84
+ - 10 span display classifications using `elinaobs.observation.type`.
85
+ - Selective export: only spans starting inside a `@trace()` scope.
86
+ - OTLP/HTTP protobuf, bearer-token auth, background batching.
87
+ - Optional bounded input/output capture, disabled by default.
88
+ - An optional `redact` callback can remove secrets from captured free text before export.
89
+ - `attribute_allowlist` and `attribute_denylist` filter span, event, and link attributes in ElinaObs exports; the denylist wins when both contain a key.
90
+ - Exceptions automatically record OTel error status/events.
91
+ - Shares an existing OpenTelemetry SDK TracerProvider when present.
92
+ - `flush()` reports failed export batches or dropped spans. `diagnose()` returns `failed_batches`, `dropped_spans`, and `retry_attempts` without exposing credentials. Drop counts are updated after a successful processor flush; retry attempts count extra HTTP requests made for a batch.
93
+
94
+ ## Non-goals for v0.1
95
+
96
+ - Framework adapters or `integrations=[...]`.
97
+ - Raw threads, multiprocessing, distributed orchestration, or generator tracing.
98
+ - Automatic patching of uninstrumented Python libraries.
99
+ - Prompt management, evaluations, or server/UI implementation.
100
+
101
+ ## Known limitations before production
102
+
103
+ - SDK processor cannot modify existing OTel provider sampling or resources.
104
+ - Capture filtering uses a ContextVar marker; parent spans begun before a
105
+ decorator scope are not exported to ElinaObs unless also eligible.
106
+ - `as_type="event"` is a timed span category, not an OTel instant event.
107
+ - Incoming external spans from `opentelemetry-instrument` share global provider
108
+ and may require controlling initialization order.
109
+ - Bounded batches can drop data on overload. `dropped_spans` is current only
110
+ after `flush()` or `shutdown()`. `retry_attempts` is `None` when an OpenTelemetry
111
+ credential-provider session is configured. Export failures remain counted
112
+ even if later batches succeed.
113
+ - Input/output redaction is best-effort only. Explicitly inspect anything you
114
+ opt in to collect.
115
+ - Attribute lists do not change an application's other OpenTelemetry exporters
116
+ or resource metadata. Keys are matched exactly.
117
+ - `shutdown()` ends only ElinaObs's processor. Do not reinitialize the SDK in
118
+ the same interpreter after shutdown.
119
+
120
+ See [docs/LLD.md](docs/LLD.md) for the finalized architecture and build checklist.
@@ -0,0 +1,100 @@
1
+ # ElinaObs Python SDK
2
+
3
+ Python tracing SDK for self-hosted Elina. Licensed under MIT.
4
+
5
+ ## Install
6
+
7
+ Once published on PyPI:
8
+
9
+ ```bash
10
+ python -m pip install elinaobs
11
+ ```
12
+
13
+ For development from this repository:
14
+
15
+ ```bash
16
+ python -m pip install -e '.[dev]'
17
+ pytest -q
18
+ ```
19
+
20
+ ## Public API
21
+
22
+ ```python
23
+ from elinaobs import init, trace, flush, shutdown, diagnose, submit_feedback
24
+
25
+ init(api_key="...", endpoint="http://localhost:3000/v1/traces", service_name="my-agent")
26
+
27
+ @trace(name="Agent", as_type="agent")
28
+ async def run(question):
29
+ return await your_code(question)
30
+
31
+ # The existing OTel-instrumented operations called inside run() are children.
32
+ ```
33
+
34
+ Application feedback is optional. After a trace has finished, an app with a verdict can send it using the same project key. Inside a decorated function, capture the trace ID and keep it with the result:
35
+
36
+ ```python
37
+ from opentelemetry import trace as otel_trace
38
+ trace_id = f"{otel_trace.get_current_span().get_span_context().trace_id:032x}"
39
+ # Later, after the traced function returns:
40
+ submit_feedback(trace_id, "bad", note="Wrong answer") # trace_id is 32 lowercase hex characters
41
+ ```
42
+
43
+ Apps without a feedback signal do not call this function. Feedback is stored separately from the dashboard's human review. Keep the project key in server-side code, not in a browser.
44
+
45
+ To send a demo trace to the local Elina server, create a project in the dashboard and copy its one-time ingestion key. Then, from this directory in PowerShell:
46
+
47
+ ```powershell
48
+ $env:ELINA_WRITE_KEY = "paste-project-ingestion-key"
49
+ $env:ELINA_BASE_URL = "http://127.0.0.1:3000"
50
+ python examples/agent.py
51
+ ```
52
+
53
+ The script prints the trace ID and reports whether export succeeded. Open the project’s Traces page and select that ID to inspect the three spans and their captured input/output.
54
+
55
+ Only operations that are explicitly decorated or already instrumented by a compatible OpenTelemetry instrumentation package create spans. **No framework integrations are installed, imported, or configured by ElinaObs.**
56
+
57
+ The Elina receiver lives in the sibling `elina-server` directory. Start it before running `examples/agent.py`. The default tests use in-memory and local HTTP receivers.
58
+
59
+ To also verify export through a real OpenTelemetry Collector, set `OTELCOL_BINARY` to the Collector executable path and run `python -m pytest -q tests/test_collector.py`. This optional test starts a local Collector with a file exporter and checks the received spans.
60
+
61
+ ## What it does
62
+
63
+ - Sync + async function decorator with native OTel parent context.
64
+ - 10 span display classifications using `elinaobs.observation.type`.
65
+ - Selective export: only spans starting inside a `@trace()` scope.
66
+ - OTLP/HTTP protobuf, bearer-token auth, background batching.
67
+ - Optional bounded input/output capture, disabled by default.
68
+ - An optional `redact` callback can remove secrets from captured free text before export.
69
+ - `attribute_allowlist` and `attribute_denylist` filter span, event, and link attributes in ElinaObs exports; the denylist wins when both contain a key.
70
+ - Exceptions automatically record OTel error status/events.
71
+ - Shares an existing OpenTelemetry SDK TracerProvider when present.
72
+ - `flush()` reports failed export batches or dropped spans. `diagnose()` returns `failed_batches`, `dropped_spans`, and `retry_attempts` without exposing credentials. Drop counts are updated after a successful processor flush; retry attempts count extra HTTP requests made for a batch.
73
+
74
+ ## Non-goals for v0.1
75
+
76
+ - Framework adapters or `integrations=[...]`.
77
+ - Raw threads, multiprocessing, distributed orchestration, or generator tracing.
78
+ - Automatic patching of uninstrumented Python libraries.
79
+ - Prompt management, evaluations, or server/UI implementation.
80
+
81
+ ## Known limitations before production
82
+
83
+ - SDK processor cannot modify existing OTel provider sampling or resources.
84
+ - Capture filtering uses a ContextVar marker; parent spans begun before a
85
+ decorator scope are not exported to ElinaObs unless also eligible.
86
+ - `as_type="event"` is a timed span category, not an OTel instant event.
87
+ - Incoming external spans from `opentelemetry-instrument` share global provider
88
+ and may require controlling initialization order.
89
+ - Bounded batches can drop data on overload. `dropped_spans` is current only
90
+ after `flush()` or `shutdown()`. `retry_attempts` is `None` when an OpenTelemetry
91
+ credential-provider session is configured. Export failures remain counted
92
+ even if later batches succeed.
93
+ - Input/output redaction is best-effort only. Explicitly inspect anything you
94
+ opt in to collect.
95
+ - Attribute lists do not change an application's other OpenTelemetry exporters
96
+ or resource metadata. Keys are matched exactly.
97
+ - `shutdown()` ends only ElinaObs's processor. Do not reinitialize the SDK in
98
+ the same interpreter after shutdown.
99
+
100
+ See [docs/LLD.md](docs/LLD.md) for the finalized architecture and build checklist.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "elinaobs"
7
+ license = "MIT"
8
+ version = "0.1.0"
9
+ description = "Framework-agnostic Python LLM tracing using OpenTelemetry"
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ authors = [{name = "ElinaObs Contributors"}]
13
+ dependencies = [
14
+ "opentelemetry-api>=1.36,<2",
15
+ "opentelemetry-sdk>=1.36,<2",
16
+ "opentelemetry-exporter-otlp-proto-http>=1.36,<2",
17
+ "requests>=2,<3",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.10", "opentelemetry-instrumentation-requests>=0.57b0,<1"]
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.pytest.ini_options]
27
+ testpaths = ["tests"]
28
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """ElinaObs Python SDK: decorator first, framework agnostic, OTLP native."""
2
+ from .decorators import trace
3
+ from .runtime import diagnose, flush, init, shutdown, submit_feedback
4
+ from .types import ObservationType
5
+
6
+ __all__ = ["init", "trace", "flush", "shutdown", "diagnose", "submit_feedback", "ObservationType"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """ElinaObs capture selection, NOT a replacement for OTel parent context."""
2
+ from contextvars import ContextVar
3
+
4
+ # Inherited by normal sync nesting and asyncio tasks.
5
+ # The OTel context still owns trace and parent/span IDs.
6
+ capture_active: ContextVar[bool] = ContextVar("elinaobs_capture_active", default=False)
@@ -0,0 +1,106 @@
1
+ """Framework-agnostic decorator for ordinary sync and async Python functions."""
2
+ from __future__ import annotations
3
+
4
+ import functools
5
+ import inspect
6
+ from collections.abc import Callable
7
+ from typing import Any, TypeVar, cast, overload
8
+
9
+ from opentelemetry import trace as otel_trace
10
+
11
+ from .context import capture_active
12
+ from .runtime import get_config
13
+ from .serialization import serialize
14
+ from .types import OBSERVATION_TYPES, ObservationType
15
+
16
+ F = TypeVar("F", bound=Callable[..., Any])
17
+
18
+
19
+ @overload
20
+ def trace(func: F, /) -> F: ...
21
+
22
+
23
+ @overload
24
+ def trace(*, name: str | None = None, as_type: ObservationType = "span") -> Callable[[F], F]: ...
25
+
26
+
27
+ def trace(
28
+ func: F | None = None,
29
+ /,
30
+ *,
31
+ name: str | None = None,
32
+ as_type: ObservationType = "span",
33
+ ) -> F | Callable[[F], F]:
34
+ """Time a Python function and attach any OTel-instrumented children.
35
+
36
+ This v0.1 deliberately rejects generator functions. Their lifetime is
37
+ iteration, not function invocation, so naïve wrappers would lie.
38
+ """
39
+ if as_type not in OBSERVATION_TYPES:
40
+ raise ValueError(f"Unknown observation type: {as_type!r}")
41
+ if name is not None and not name.strip():
42
+ raise ValueError("name must be nonempty")
43
+
44
+ def decorate(fn: F) -> F:
45
+ if inspect.isgeneratorfunction(fn) or inspect.isasyncgenfunction(fn):
46
+ raise TypeError("@trace does not support generator functions in v0.1")
47
+ label = name or fn.__qualname__
48
+
49
+ def attach_input(span, args, kwargs, settings):
50
+ if not settings.capture_input or not span.is_recording():
51
+ return
52
+ try:
53
+ bound = inspect.signature(fn).bind_partial(*args, **kwargs)
54
+ value = dict(bound.arguments)
55
+ value.pop("self", None)
56
+ value.pop("cls", None)
57
+ span.set_attribute("elinaobs.input", serialize(value, max_chars=settings.max_content_chars, redact=settings.redact))
58
+ except Exception: # instrumentation must not break the application
59
+ pass
60
+
61
+ def attach_output(span, value, settings):
62
+ if not settings.capture_output or not span.is_recording():
63
+ return
64
+ try:
65
+ span.set_attribute("elinaobs.output", serialize(value, max_chars=settings.max_content_chars, redact=settings.redact))
66
+ except Exception:
67
+ pass
68
+
69
+ if inspect.iscoroutinefunction(fn):
70
+ @functools.wraps(fn)
71
+ async def async_wrapper(*args, **kwargs):
72
+ settings = get_config()
73
+ token = capture_active.set(True)
74
+ try:
75
+ tracer = otel_trace.get_tracer("elinaobs.sdk", "0.1.0")
76
+ with tracer.start_as_current_span(
77
+ label, attributes={"elinaobs.observation.type": as_type}
78
+ ) as span:
79
+ attach_input(span, args, kwargs, settings)
80
+ output = await fn(*args, **kwargs)
81
+ attach_output(span, output, settings)
82
+ return output
83
+ finally:
84
+ capture_active.reset(token)
85
+ return cast(F, async_wrapper)
86
+
87
+ @functools.wraps(fn)
88
+ def sync_wrapper(*args, **kwargs):
89
+ settings = get_config()
90
+ token = capture_active.set(True)
91
+ try:
92
+ tracer = otel_trace.get_tracer("elinaobs.sdk", "0.1.0")
93
+ with tracer.start_as_current_span(
94
+ label, attributes={"elinaobs.observation.type": as_type}
95
+ ) as span:
96
+ attach_input(span, args, kwargs, settings)
97
+ output = fn(*args, **kwargs)
98
+ attach_output(span, output, settings)
99
+ return output
100
+ finally:
101
+ capture_active.reset(token)
102
+ return cast(F, sync_wrapper)
103
+
104
+ if func is not None:
105
+ return decorate(func)
106
+ return decorate
@@ -0,0 +1,57 @@
1
+ """Selectively forward spans created inside @trace scope to an OTel processor."""
2
+ from __future__ import annotations
3
+
4
+ from threading import Lock
5
+
6
+ from opentelemetry.sdk.trace import SpanProcessor
7
+
8
+ from .context import capture_active
9
+
10
+
11
+ class ScopedSpanProcessor(SpanProcessor):
12
+ """Keep our exporter from sending unrelated global OTel spans.
13
+
14
+ on_start runs synchronously while the calling ContextVar is available.
15
+ on_end may run after the capture scope has exited, so eligibility is
16
+ remembered by the pair (trace_id, span_id), not re-checked on end.
17
+ """
18
+
19
+ def __init__(self, downstream: SpanProcessor) -> None:
20
+ self._downstream = downstream
21
+ self._eligible: set[tuple[int, int]] = set()
22
+ self._lock = Lock()
23
+ self.ended_spans = 0
24
+ self.flushed_spans = 0
25
+
26
+ @staticmethod
27
+ def _key(span) -> tuple[int, int]:
28
+ ctx = span.get_span_context()
29
+ return (ctx.trace_id, ctx.span_id)
30
+
31
+ def on_start(self, span, parent_context=None) -> None:
32
+ if not capture_active.get():
33
+ return
34
+ key = self._key(span)
35
+ with self._lock:
36
+ self._eligible.add(key)
37
+ self._downstream.on_start(span, parent_context=parent_context)
38
+
39
+ def on_end(self, span) -> None:
40
+ key = self._key(span)
41
+ with self._lock:
42
+ if key not in self._eligible:
43
+ return
44
+ self._eligible.remove(key)
45
+ if span.context.trace_flags.sampled:
46
+ self.ended_spans += 1
47
+ self._downstream.on_end(span)
48
+
49
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
50
+ with self._lock:
51
+ result = self._downstream.force_flush(timeout_millis=timeout_millis)
52
+ if result:
53
+ self.flushed_spans = self.ended_spans
54
+ return result
55
+
56
+ def shutdown(self) -> None:
57
+ self._downstream.shutdown()