tracely-sdk 0.1.0__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.
tracely/tracing.py ADDED
@@ -0,0 +1,59 @@
1
+ """Public tracing API for custom spans (FR58, FR59).
2
+
3
+ Provides the `span()` context manager for developers to create
4
+ custom spans within their application code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from contextlib import contextmanager
10
+ from typing import Any, Callable, Generator
11
+
12
+ from tracely.context import get_current_span, _span_context
13
+ from tracely.span import Span
14
+ from tracely.span_processor import on_span_end, on_span_start
15
+
16
+
17
+ @contextmanager
18
+ def span(
19
+ name: str,
20
+ *,
21
+ kind: str = "INTERNAL",
22
+ service_name: str | None = None,
23
+ on_end: Callable[[Span], None] | None = None,
24
+ ) -> Generator[Span, None, None]:
25
+ """Create a custom span as a child of the currently active span.
26
+
27
+ If no span is active, creates a root span. The span is automatically
28
+ ended when the context exits (including on exception).
29
+
30
+ Usage::
31
+
32
+ with tracely.span("my-operation") as s:
33
+ s.set_attribute("key", "value")
34
+ # do work
35
+ # span is ended here
36
+
37
+ Args:
38
+ name: Human-readable operation name.
39
+ kind: Span kind (INTERNAL, CLIENT, PRODUCER, CONSUMER).
40
+ service_name: Optional service name override.
41
+ on_end: Optional callback invoked when span ends.
42
+ """
43
+ parent = get_current_span()
44
+ s = Span(
45
+ name=name,
46
+ parent=parent,
47
+ kind=kind,
48
+ service_name=service_name,
49
+ on_end=on_end or on_span_end,
50
+ )
51
+
52
+ # AR3: Export pending_span immediately for real-time dashboard
53
+ on_span_start(s)
54
+
55
+ with _span_context(s):
56
+ try:
57
+ yield s
58
+ finally:
59
+ s.end()
tracely/transport.py ADDED
@@ -0,0 +1,134 @@
1
+ """HTTP transport with buffering and retry for span data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from collections import deque
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ logger = logging.getLogger("tracely")
13
+
14
+ DEFAULT_BATCH_SIZE = 50
15
+ DEFAULT_MAX_BUFFER = 1000
16
+ DEFAULT_MAX_RETRIES = 3
17
+ DEFAULT_BASE_DELAY = 1.0
18
+ DEFAULT_MAX_DELAY = 30.0
19
+
20
+
21
+ class SpanBuffer:
22
+ """Thread-safe in-memory buffer for span data.
23
+
24
+ Drops oldest spans when max_size is exceeded.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ max_size: int = DEFAULT_MAX_BUFFER,
30
+ batch_size: int = DEFAULT_BATCH_SIZE,
31
+ ) -> None:
32
+ self._buffer: deque[dict[str, Any]] = deque(maxlen=max_size)
33
+ self._batch_size = batch_size
34
+
35
+ def enqueue(self, span: dict[str, Any]) -> None:
36
+ """Add a span to the buffer. Oldest dropped if full."""
37
+ self._buffer.append(span)
38
+
39
+ def flush(self) -> list[dict[str, Any]]:
40
+ """Return all buffered spans and clear the buffer."""
41
+ spans = list(self._buffer)
42
+ self._buffer.clear()
43
+ return spans
44
+
45
+ @property
46
+ def size(self) -> int:
47
+ """Number of spans currently in buffer."""
48
+ return len(self._buffer)
49
+
50
+ @property
51
+ def is_ready(self) -> bool:
52
+ """Buffer has reached the batch threshold."""
53
+ return len(self._buffer) >= self._batch_size
54
+
55
+
56
+ class HttpTransport:
57
+ """Sends span data to the TRACELY API with retry and backoff.
58
+
59
+ All errors are caught silently — the host application must never be
60
+ affected by transport failures (FR10, NFR22).
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ endpoint: str,
66
+ api_key: str,
67
+ max_retries: int = DEFAULT_MAX_RETRIES,
68
+ base_delay: float = DEFAULT_BASE_DELAY,
69
+ max_delay: float = DEFAULT_MAX_DELAY,
70
+ ) -> None:
71
+ self._endpoint = endpoint.rstrip("/")
72
+ self._api_key = api_key
73
+ self._max_retries = max_retries
74
+ self._base_delay = base_delay
75
+ self._max_delay = max_delay
76
+ self._client = httpx.AsyncClient(
77
+ timeout=httpx.Timeout(10.0),
78
+ headers={
79
+ "Authorization": f"Bearer {api_key}",
80
+ "Content-Type": "application/x-protobuf",
81
+ },
82
+ )
83
+
84
+ async def send(self, payload: bytes) -> bool:
85
+ """Send OTLP protobuf payload to the API. Returns True on success.
86
+
87
+ Args:
88
+ payload: Serialized OTLP ExportTraceServiceRequest bytes.
89
+
90
+ Never raises — all exceptions are caught and logged (FR10, NFR22).
91
+ """
92
+ if not payload:
93
+ return True
94
+
95
+ url = f"{self._endpoint}/v1/traces"
96
+ attempt = 0
97
+ max_attempts = 1 + self._max_retries
98
+
99
+ while attempt < max_attempts:
100
+ try:
101
+ response = await self._client.post(url, content=payload)
102
+ response.raise_for_status()
103
+ return True
104
+ except (httpx.HTTPStatusError, httpx.ConnectError, httpx.TimeoutException, ConnectionError, OSError) as exc:
105
+ attempt += 1
106
+ if attempt < max_attempts:
107
+ delay = min(
108
+ self._base_delay * (2 ** (attempt - 1)),
109
+ self._max_delay,
110
+ )
111
+ logger.debug(
112
+ "TRACELY transport retry %d/%d after %.1fs: %s",
113
+ attempt,
114
+ self._max_retries,
115
+ delay,
116
+ exc,
117
+ )
118
+ await asyncio.sleep(delay)
119
+ else:
120
+ logger.debug(
121
+ "TRACELY transport failed after %d attempts: %s",
122
+ max_attempts,
123
+ exc,
124
+ )
125
+ except Exception as exc:
126
+ # Catch-all: SDK must never crash the host app
127
+ logger.debug("TRACELY transport unexpected error: %s", exc)
128
+ return False
129
+
130
+ return False
131
+
132
+ async def close(self) -> None:
133
+ """Close the HTTP client."""
134
+ await self._client.aclose()
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: tracely-sdk
3
+ Version: 0.1.0
4
+ Summary: Lightweight observability SDK for Python web frameworks. Auto-instruments FastAPI, Flask, and Django with real-time tracing via OTLP/HTTP.
5
+ Project-URL: Homepage, https://tracely.sh
6
+ Project-URL: Documentation, https://docs.tracely.sh
7
+ Project-URL: Repository, https://github.com/TracelyOrg/tracely-sdk
8
+ Project-URL: Issues, https://github.com/TracelyOrg/tracely-sdk/issues
9
+ Author-email: Charles Dzadu <contact@tracely.sh>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: apm,django,fastapi,flask,monitoring,observability,opentelemetry,tracing
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Framework :: Flask
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: System :: Monitoring
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: httpx>=0.27
29
+ Requires-Dist: opentelemetry-proto>=1.20
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: respx>=0.21; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # tracely-sdk
37
+
38
+ Lightweight observability SDK for Python web frameworks. Auto-instruments **FastAPI**, **Flask**, and **Django** with real-time distributed tracing via OTLP/HTTP.
39
+
40
+ [![PyPI version](https://img.shields.io/pypi/v/tracely-sdk.svg)](https://pypi.org/project/tracely-sdk/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/tracely-sdk.svg)](https://pypi.org/project/tracely-sdk/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
43
+
44
+ ## Features
45
+
46
+ - **Zero-config auto-instrumentation** -- detects FastAPI, Flask, and Django automatically
47
+ - **Real-time pending spans** -- see requests the moment they start, not just when they finish
48
+ - **Full request/response capture** -- headers, body, query params with smart redaction
49
+ - **OTLP/HTTP protobuf export** -- standard OpenTelemetry wire format
50
+ - **Batch export with backoff** -- 1s flush interval, exponential retry on failure
51
+ - **Fail-silent design** -- SDK never crashes or degrades your application
52
+ - **Minimal dependencies** -- only `httpx` and `opentelemetry-proto`
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install tracely-sdk
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ### FastAPI
63
+
64
+ ```python
65
+ import tracely
66
+ from tracely.instrumentation.fastapi_inst import TracelyASGIMiddleware
67
+
68
+ tracely.init(api_key="trly_your_key_here")
69
+
70
+ from fastapi import FastAPI
71
+ app = FastAPI()
72
+ app.add_middleware(TracelyASGIMiddleware)
73
+
74
+ @app.get("/")
75
+ async def root():
76
+ return {"status": "ok"}
77
+ ```
78
+
79
+ ### Flask
80
+
81
+ ```python
82
+ import tracely
83
+ from tracely.instrumentation.flask_inst import FlaskInstrumentor
84
+
85
+ tracely.init(api_key="trly_your_key_here")
86
+
87
+ from flask import Flask
88
+ app = Flask(__name__)
89
+ app.wsgi_app = FlaskInstrumentor.wrap_app(app.wsgi_app)
90
+
91
+ @app.route("/")
92
+ def root():
93
+ return {"status": "ok"}
94
+ ```
95
+
96
+ ### Django
97
+
98
+ Add the middleware to your `MIDDLEWARE` setting:
99
+
100
+ ```python
101
+ # settings.py
102
+ MIDDLEWARE = [
103
+ "tracely.instrumentation.django_inst.TracelyDjangoMiddleware",
104
+ # ... other middleware
105
+ ]
106
+ ```
107
+
108
+ Then initialize in your app startup (e.g., `AppConfig.ready()`):
109
+
110
+ ```python
111
+ import tracely
112
+ tracely.init(api_key="trly_your_key_here")
113
+ ```
114
+
115
+ ## Configuration
116
+
117
+ ### Environment Variables
118
+
119
+ | Variable | Description | Default |
120
+ |----------|-------------|---------|
121
+ | `TRACELY_API_KEY` | API key for authentication | _(required)_ |
122
+ | `TRACELY_ENDPOINT` | Ingestion API endpoint | `https://i.tracely.sh` |
123
+ | `ENVIRONMENT` | Deployment environment (e.g., `production`) | `None` |
124
+ | `TRACELY_REDACT_FIELDS` | Comma-separated header/field names to redact | `None` |
125
+
126
+ ### Programmatic Init
127
+
128
+ ```python
129
+ import tracely
130
+
131
+ tracely.init(
132
+ api_key="trly_your_key_here",
133
+ environment="production",
134
+ service_name="my-api",
135
+ service_version="1.0.0",
136
+ )
137
+ ```
138
+
139
+ ## Custom Spans
140
+
141
+ Create manual spans for custom operations:
142
+
143
+ ```python
144
+ import tracely
145
+
146
+ with tracely.span("db-query", kind="CLIENT") as s:
147
+ s.set_attribute("db.system", "postgres")
148
+ s.set_attribute("db.statement", "SELECT * FROM users")
149
+ result = db.execute("SELECT * FROM users")
150
+ ```
151
+
152
+ ## Span Events (Structured Logging)
153
+
154
+ Attach structured log events to the active span:
155
+
156
+ ```python
157
+ import tracely
158
+
159
+ with tracely.span("process-order") as s:
160
+ tracely.info("Order received", order_id="123")
161
+ # ... process
162
+ tracely.debug("Validation passed")
163
+ tracely.warning("Inventory low", sku="WIDGET-42")
164
+ ```
165
+
166
+ ## Graceful Shutdown
167
+
168
+ Flush buffered spans before exit:
169
+
170
+ ```python
171
+ import tracely
172
+
173
+ tracely.init(api_key="trly_your_key_here")
174
+ # ... application runs ...
175
+ tracely.shutdown() # flushes remaining spans
176
+ ```
177
+
178
+ ## How It Works
179
+
180
+ ```
181
+ Your App
182
+ |
183
+ v
184
+ Middleware (FastAPI/Flask/Django)
185
+ |-- on_start --> SpanProcessor --> SpanBuffer (pending_span)
186
+ |-- on_end ----> SpanProcessor --> SpanBuffer (final span)
187
+ |
188
+ v
189
+ BatchSpanExporter (1s interval / 50 span threshold)
190
+ |
191
+ v
192
+ OTLP Protobuf Serialization
193
+ |
194
+ v
195
+ HttpTransport --> TRACELY API
196
+ (retry with exponential backoff)
197
+ ```
198
+
199
+ ## Documentation
200
+
201
+ Full docs at [docs.tracely.sh](https://docs.tracely.sh)
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,28 @@
1
+ tracely/__init__.py,sha256=7CmCMoZ7kaz2nE7o-iRfZ4z6P2_LrVX9LlCGez4-sac,357
2
+ tracely/capture.py,sha256=_W0V1pdx9dUIQWwafOggm2hDisyfpY_fJsgz1H7Poyo,6636
3
+ tracely/config.py,sha256=F_ytza_-HEpn6rGxb8kBAbuoLrQu88BIey7SpZP9vJ0,1244
4
+ tracely/context.py,sha256=PjxtEph9ooC0T6jOLnX6a16nnThHlWg52183O-wV2bE,1613
5
+ tracely/detection.py,sha256=BI2z2b2ncfEPMA4_JmDulpxMGctQBy-rZSf2xT3JFvs,1460
6
+ tracely/exporter.py,sha256=ce3jfMzBHaa38YgG-I7CP6Q7dwljP4VanWXVPZP4LpM,4018
7
+ tracely/log_handler.py,sha256=BYxUnRTim1D8L79QOlALD9W_feLjSul8oMKrZiTGnys,1776
8
+ tracely/logging_api.py,sha256=KKGRu_mb2XZk7tgzUYWRl6bh0_G42KAar-X2yH_Wm9I,1264
9
+ tracely/otlp.py,sha256=pfeQVN641ZusnovMYz6A4_XkFqcblwO5hT4Cda8LLhQ,4265
10
+ tracely/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ tracely/redaction.py,sha256=ywsjD7o20RWu_qS6PYt3WnbxrJslHGA3IeAGS96jpTw,5949
12
+ tracely/sdk.py,sha256=-zvNUFw1Nx4KxR2i90FlW4ELSh0R0Cb2VFuG_o1zWbU,6072
13
+ tracely/span.py,sha256=bWo8LM_vR0Uc1g6o-6-wdYOnwPJ3UQENtcWdGumSwaI,5079
14
+ tracely/span_processor.py,sha256=P4f4192t-FBppw9LmPebHA1ciA5UKp4eseTY-3LAPWA,3370
15
+ tracely/tracing.py,sha256=NiwcrZ9S8-fqVWfYgc6PTlzc141v-Om311TOytowph4,1608
16
+ tracely/transport.py,sha256=mvrA9JqVmHydAs-Z_iPypBNywX8UgZYjooAU-HS11sQ,4131
17
+ tracely/instrumentation/__init__.py,sha256=QPCafxwszvO8nwLrsMkwcsD0j3hg_OG3T5GAd4DYr_E,1476
18
+ tracely/instrumentation/base.py,sha256=g5siRo1L6K88rrb47OS9tmg5riNtljEMlRUy1BENqdQ,860
19
+ tracely/instrumentation/dbapi.py,sha256=ax_wG5PUOASARKay3CCTQBGFeY9rCUBsdNp_Rel9tsc,8882
20
+ tracely/instrumentation/django_inst.py,sha256=wYO5c-TYW9ScyWLnCQw8UHiyKgx-OE0Ie9lNqrwP_X4,5560
21
+ tracely/instrumentation/fastapi_inst.py,sha256=1h4Kd6DTig4CcNFoVKclh0TjAHCFqcAJDzDpUiQMSSQ,7388
22
+ tracely/instrumentation/flask_inst.py,sha256=fCfC-hYKQy1pSNwy3e6VZQ0KF93F82CqnNAFnf-J_Bo,7646
23
+ tracely/instrumentation/generic.py,sha256=hjHlnVrJQXsXtVR3P0mVehf9OqAZ6UZFIR-GI0D0qeQ,1021
24
+ tracely/instrumentation/httpx_inst.py,sha256=hQ25EE37G7EoyLpR9g9r5Z5qVUgw5e4D5fTPKsSiZuE,4825
25
+ tracely_sdk-0.1.0.dist-info/METADATA,sha256=ks5_rxp34HGmqbEMC3tCGo5Y1kl5ryt9amimzY-d2tM,5889
26
+ tracely_sdk-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
27
+ tracely_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=btzJ8TFbmXLjXC326W6a5ta0IBUtxUAo1X-mCFW_EhQ,1075
28
+ tracely_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Charles Dzadu
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.