custodia-sdk 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,21 @@
1
+ Copyright (c) 2026 Neurona. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are the
4
+ proprietary property of Neurona ("Licensor"). The Software is made available
5
+ for download and inspection only. No license, right, or permission to use,
6
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software is granted except as expressly agreed in a separate written
8
+ commercial license agreement with Licensor.
9
+
10
+ Use of the Software without a valid, active commercial license from Licensor
11
+ is strictly prohibited.
12
+
13
+ To obtain a commercial license, contact: michael.gunawan2002@gmail.com
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.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: custodia-sdk
3
+ Version: 0.1.0
4
+ Summary: Custodia tracing SDK
5
+ Author: ssabrut
6
+ Author-email: ssabrut <michael.gunawan2002@gmail.com>
7
+ License-Expression: LicenseRef-Proprietary
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.44.0
13
+ Requires-Dist: opentelemetry-sdk>=1.44.0
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+
17
+ # custodia-sdk
18
+
19
+ Python tracing SDK for Custodia. Instruments function calls as OpenTelemetry
20
+ spans and exports them via OTLP/HTTP.
21
+
22
+ > **License:** proprietary. Use requires a paid commercial license from
23
+ > Neurona. See [LICENSE](./LICENSE). Contact michael.gunawan2002@gmail.com
24
+ > to obtain one.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install custodia-sdk
30
+ # or
31
+ uv add custodia-sdk
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ Set via environment variables:
37
+
38
+ | Variable | Description | Default |
39
+ |---|---|---|
40
+ | `CUSTODIA_INGEST_URL` | OTLP/HTTP traces endpoint | `http://localhost:4318/v1/traces` |
41
+ | `CUSTODIA_API_KEY` | Bearer token sent with exported spans | *(none)* |
42
+ | `CUSTODIA_SERVICE_NAME` | `service.name` resource attribute | `unknown-service` |
43
+
44
+ ## Usage
45
+
46
+ ```python
47
+ from custodia import trace, trace_async, trace_span, atrace_span
48
+
49
+ @trace(name="fetch_user", metadata={"component": "db"})
50
+ def get_user(user_id: str):
51
+ return db.query(user_id)
52
+
53
+ @trace_async(name="call_llm")
54
+ async def generate(prompt: str) -> str:
55
+ return await llm_client.complete(prompt)
56
+
57
+ with trace_span("parse_response") as span:
58
+ data = json.loads(raw)
59
+ span.set_attribute("record_count", len(data))
60
+
61
+ async with atrace_span("call_downstream") as span:
62
+ resp = await http_client.get(url)
63
+ span.set_attribute("http.status_code", resp.status_code)
64
+ ```
65
+
66
+ - `trace` / `trace_async`: decorators that auto-capture args/return value as
67
+ `gen_ai.prompt` / `gen_ai.completion` span attributes.
68
+ - `trace_span` / `atrace_span`: context managers for manual span control,
69
+ no automatic I/O capture.
@@ -0,0 +1,53 @@
1
+ # custodia-sdk
2
+
3
+ Python tracing SDK for Custodia. Instruments function calls as OpenTelemetry
4
+ spans and exports them via OTLP/HTTP.
5
+
6
+ > **License:** proprietary. Use requires a paid commercial license from
7
+ > Neurona. See [LICENSE](./LICENSE). Contact michael.gunawan2002@gmail.com
8
+ > to obtain one.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install custodia-sdk
14
+ # or
15
+ uv add custodia-sdk
16
+ ```
17
+
18
+ ## Configuration
19
+
20
+ Set via environment variables:
21
+
22
+ | Variable | Description | Default |
23
+ |---|---|---|
24
+ | `CUSTODIA_INGEST_URL` | OTLP/HTTP traces endpoint | `http://localhost:4318/v1/traces` |
25
+ | `CUSTODIA_API_KEY` | Bearer token sent with exported spans | *(none)* |
26
+ | `CUSTODIA_SERVICE_NAME` | `service.name` resource attribute | `unknown-service` |
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from custodia import trace, trace_async, trace_span, atrace_span
32
+
33
+ @trace(name="fetch_user", metadata={"component": "db"})
34
+ def get_user(user_id: str):
35
+ return db.query(user_id)
36
+
37
+ @trace_async(name="call_llm")
38
+ async def generate(prompt: str) -> str:
39
+ return await llm_client.complete(prompt)
40
+
41
+ with trace_span("parse_response") as span:
42
+ data = json.loads(raw)
43
+ span.set_attribute("record_count", len(data))
44
+
45
+ async with atrace_span("call_downstream") as span:
46
+ resp = await http_client.get(url)
47
+ span.set_attribute("http.status_code", resp.status_code)
48
+ ```
49
+
50
+ - `trace` / `trace_async`: decorators that auto-capture args/return value as
51
+ `gen_ai.prompt` / `gen_ai.completion` span attributes.
52
+ - `trace_span` / `atrace_span`: context managers for manual span control,
53
+ no automatic I/O capture.
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "custodia-sdk"
3
+ version = "0.1.0"
4
+ description = "Custodia tracing SDK"
5
+ readme = "README.md"
6
+ license = "LicenseRef-Proprietary"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "ssabrut", email = "michael.gunawan2002@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "httpx>=0.28.1",
14
+ "opentelemetry-exporter-otlp-proto-http>=1.44.0",
15
+ "opentelemetry-sdk>=1.44.0",
16
+ ]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "Operating System :: OS Independent",
20
+ ]
21
+
22
+ [tool.uv.build-backend]
23
+ module-name = "custodia"
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.11.30,<0.12.0"]
27
+ build-backend = "uv_build"
@@ -0,0 +1,4 @@
1
+ from ._tracer import get_tracer
2
+ from .trace import atrace_span, trace, trace_async, trace_span
3
+
4
+ __all__ = ["trace", "trace_async", "trace_span", "atrace_span", "get_tracer"]
@@ -0,0 +1,36 @@
1
+ import os
2
+
3
+ from opentelemetry import trace as otel_trace
4
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
5
+ from opentelemetry.sdk.resources import Resource
6
+ from opentelemetry.sdk.trace import TracerProvider
7
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
8
+
9
+ _initialized = False
10
+
11
+
12
+ def _ensure_initialized() -> None:
13
+ global _initialized
14
+ if _initialized:
15
+ return
16
+
17
+ endpoint = os.environ.get("CUSTODIA_INGEST_URL", "http://localhost:4318/v1/traces")
18
+ api_key = os.environ.get("CUSTODIA_API_KEY", "")
19
+
20
+ provider = TracerProvider(
21
+ resource=Resource.create(
22
+ {"service.name": os.environ.get("CUSTODIA_SERVICE_NAME", "unknown-service")}
23
+ )
24
+ )
25
+ exporter = OTLPSpanExporter(
26
+ endpoint=endpoint,
27
+ headers={"Authorization": f"Bearer {api_key}"} if api_key else {},
28
+ )
29
+ provider.add_span_processor(BatchSpanProcessor(exporter))
30
+ otel_trace.set_tracer_provider(provider)
31
+ _initialized = True
32
+
33
+
34
+ def get_tracer() -> otel_trace.Tracer:
35
+ _ensure_initialized()
36
+ return otel_trace.get_tracer("custodia-sdk")
File without changes
@@ -0,0 +1,237 @@
1
+ import functools
2
+ import json
3
+ from contextlib import asynccontextmanager, contextmanager
4
+ from typing import Any, AsyncGenerator, Callable, Generator, Optional
5
+
6
+ from opentelemetry.trace import Status, StatusCode
7
+
8
+ from ._tracer import get_tracer
9
+
10
+ GENAI_PROMPT_ATTR = "gen_ai.prompt"
11
+ GENAI_COMPLETION_ATTR = "gen_ai.completion"
12
+
13
+
14
+ def _set_io_attributes(span: Any, args: tuple, kwargs: dict, result: Any) -> None:
15
+ try:
16
+ span.set_attribute(
17
+ GENAI_PROMPT_ATTR,
18
+ json.dumps({"args": _safe_repr(args), "kwargs": _safe_repr(kwargs)}),
19
+ )
20
+ span.set_attribute(GENAI_COMPLETION_ATTR, _safe_repr(result))
21
+ except TypeError:
22
+ pass
23
+
24
+
25
+ def _safe_repr(value: Any) -> str:
26
+ try:
27
+ return json.dumps(value, default=str)
28
+ except (TypeError, ValueError):
29
+ return repr(value)
30
+
31
+
32
+ def _set_span_metadata(span: Any, metadata: Optional[dict]) -> None:
33
+ if metadata:
34
+ for key, value in metadata.items():
35
+ span.set_attribute(f"metadata.{key}", _safe_repr(value))
36
+
37
+
38
+ def _record_error(span: Any, exc: Exception) -> None:
39
+ span.set_status(Status(StatusCode.ERROR, str(exc)))
40
+ span.record_exception(exc)
41
+
42
+
43
+ def trace(name: Optional[str] = None, metadata: Optional[dict] = None) -> Callable:
44
+ """Decorator that wraps a synchronous function in an OpenTelemetry span.
45
+
46
+ On each call, starts a span named `name` (or the function's `__name__`
47
+ if omitted), attaches `metadata` as `metadata.<key>` span attributes,
48
+ invokes the wrapped function, and records the call's arguments and
49
+ return value as `gen_ai.prompt` / `gen_ai.completion` span attributes
50
+ (JSON-encoded, falling back to `repr()` for non-serializable values).
51
+
52
+ If the wrapped function raises, the exception is recorded on the span,
53
+ the span status is set to ERROR, and the exception is re-raised
54
+ unchanged.
55
+
56
+ Use this for a whole function call that represents one logical
57
+ operation worth tracing (e.g. an LLM call), where capturing all
58
+ inputs/outputs automatically is desired. For tracing only part of a
59
+ function, or to control which attributes are recorded, use
60
+ `trace_span` instead.
61
+
62
+ Args:
63
+ name: Span name. Defaults to the decorated function's `__name__`.
64
+ metadata: Optional static key/value pairs attached to every span
65
+ created by this decorator, as `metadata.<key>` attributes.
66
+
67
+ Returns:
68
+ A decorator that wraps a sync function, preserving its signature
69
+ and return value via `functools.wraps`.
70
+
71
+ Example:
72
+ ```python
73
+ @trace(name="fetch_user", metadata={"component": "db"})
74
+ def get_user(user_id: str) -> User:
75
+ return db.query(user_id)
76
+ ```
77
+ """
78
+
79
+ def decorator(fn: Callable) -> Callable:
80
+ span_name = name or fn.__name__
81
+
82
+ @functools.wraps(fn)
83
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
84
+ tracer = get_tracer()
85
+ with tracer.start_as_current_span(span_name) as span:
86
+ _set_span_metadata(span, metadata)
87
+ try:
88
+ result = fn(*args, **kwargs)
89
+ except Exception as exc:
90
+ _record_error(span, exc)
91
+ raise
92
+ _set_io_attributes(span, args, kwargs, result)
93
+ return result
94
+
95
+ return wrapper
96
+
97
+ return decorator
98
+
99
+
100
+ def trace_async(name: Optional[str] = None, metadata: Optional[dict] = None) -> Callable:
101
+ """Decorator that wraps an async function in an OpenTelemetry span.
102
+
103
+ Async counterpart to `trace`. Behaves identically — starts a span,
104
+ attaches `metadata`, awaits the wrapped coroutine function, records
105
+ its arguments and return value as `gen_ai.prompt` / `gen_ai.completion`
106
+ span attributes, and records/reraises any exception with the span
107
+ marked as ERROR — but wraps a coroutine function (`async def`) instead
108
+ of a plain function.
109
+
110
+ Args:
111
+ name: Span name. Defaults to the decorated function's `__name__`.
112
+ metadata: Optional static key/value pairs attached to every span
113
+ created by this decorator, as `metadata.<key>` attributes.
114
+
115
+ Returns:
116
+ A decorator that wraps an async function, preserving its signature
117
+ and return value via `functools.wraps`.
118
+
119
+ Example:
120
+ ```python
121
+ @trace_async(name="call_llm")
122
+ async def generate(prompt: str) -> str:
123
+ return await llm_client.complete(prompt)
124
+ ```
125
+ """
126
+
127
+ def decorator(fn: Callable) -> Callable:
128
+ span_name = name or fn.__name__
129
+
130
+ @functools.wraps(fn)
131
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
132
+ tracer = get_tracer()
133
+ with tracer.start_as_current_span(span_name) as span:
134
+ _set_span_metadata(span, metadata)
135
+ try:
136
+ result = await fn(*args, **kwargs)
137
+ except Exception as exc:
138
+ _record_error(span, exc)
139
+ raise
140
+ _set_io_attributes(span, args, kwargs, result)
141
+ return result
142
+
143
+ return wrapper
144
+
145
+ return decorator
146
+
147
+
148
+ @contextmanager
149
+ def trace_span(
150
+ name: str, metadata: Optional[dict] = None
151
+ ) -> Generator[Any, None, None]:
152
+ """Context manager that starts an OpenTelemetry span around a block of
153
+ synchronous code.
154
+
155
+ Starts a span named `name`, attaches `metadata` as `metadata.<key>`
156
+ span attributes, and yields the span object to the caller for manual
157
+ attribute/event setting. Unlike `trace`, no automatic prompt/completion
158
+ (input/output) attributes are recorded — the caller has full control
159
+ over what gets attached to the span.
160
+
161
+ If an exception propagates out of the `with` block, it is recorded on
162
+ the span, the span status is set to ERROR, and the exception is
163
+ re-raised unchanged.
164
+
165
+ Use this instead of `trace` when only part of a function should be
166
+ traced, when a single function performs multiple distinct operations
167
+ that each deserve their own span, when tracing code with no dedicated
168
+ function to decorate, or when automatic capture of args/return values
169
+ is undesirable (e.g. large or sensitive payloads).
170
+
171
+ Args:
172
+ name: Span name.
173
+ metadata: Optional key/value pairs attached to the span as
174
+ `metadata.<key>` attributes.
175
+
176
+ Yields:
177
+ The active span object, for setting custom attributes
178
+ (`span.set_attribute(...)`) or recording events during the block.
179
+
180
+ Example:
181
+ ```python
182
+ with trace_span("parse_response", metadata={"format": "json"}) as span:
183
+ data = json.loads(raw)
184
+ span.set_attribute("record_count", len(data))
185
+ ```
186
+ """
187
+ tracer = get_tracer()
188
+ with tracer.start_as_current_span(name) as span:
189
+ _set_span_metadata(span, metadata)
190
+ try:
191
+ yield span
192
+ except Exception as exc:
193
+ _record_error(span, exc)
194
+ raise
195
+
196
+
197
+ @asynccontextmanager
198
+ async def atrace_span(
199
+ name: str, metadata: Optional[dict] = None
200
+ ) -> AsyncGenerator[Any, None]:
201
+ """Async context manager that starts an OpenTelemetry span around a
202
+ block of async code.
203
+
204
+ Async counterpart to `trace_span`. Starts a span named `name`, attaches
205
+ `metadata` as `metadata.<key>` span attributes, and yields the span
206
+ object for manual attribute setting inside an `async with` block. No
207
+ automatic prompt/completion attributes are recorded — same
208
+ caller-controlled attribution as `trace_span`.
209
+
210
+ If an exception propagates out of the `async with` block, it is
211
+ recorded on the span, the span status is set to ERROR, and the
212
+ exception is re-raised unchanged.
213
+
214
+ Args:
215
+ name: Span name.
216
+ metadata: Optional key/value pairs attached to the span as
217
+ `metadata.<key>` attributes.
218
+
219
+ Yields:
220
+ The active span object, for setting custom attributes
221
+ (`span.set_attribute(...)`) or recording events during the block.
222
+
223
+ Example:
224
+ ```python
225
+ async with atrace_span("call_downstream") as span:
226
+ resp = await http_client.get(url)
227
+ span.set_attribute("http.status_code", resp.status_code)
228
+ ```
229
+ """
230
+ tracer = get_tracer()
231
+ with tracer.start_as_current_span(name) as span:
232
+ _set_span_metadata(span, metadata)
233
+ try:
234
+ yield span
235
+ except Exception as exc:
236
+ _record_error(span, exc)
237
+ raise