obtrace-sdk-python 1.0.0__tar.gz → 2.0.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.
- obtrace_sdk_python-2.0.0/LICENSE +21 -0
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/PKG-INFO +22 -12
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/README.md +1 -1
- obtrace_sdk_python-2.0.0/pyproject.toml +38 -0
- obtrace_sdk_python-2.0.0/src/obtrace_sdk/__init__.py +17 -0
- obtrace_sdk_python-2.0.0/src/obtrace_sdk/client.py +169 -0
- obtrace_sdk_python-2.0.0/src/obtrace_sdk/logging_handler.py +23 -0
- obtrace_sdk_python-2.0.0/src/obtrace_sdk/otel_setup.py +142 -0
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk/types.py +1 -1
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk_python.egg-info/PKG-INFO +22 -12
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk_python.egg-info/SOURCES.txt +3 -4
- obtrace_sdk_python-2.0.0/src/obtrace_sdk_python.egg-info/requires.txt +18 -0
- obtrace_sdk_python-2.0.0/tests/test_client.py +91 -0
- obtrace_sdk_python-1.0.0/pyproject.toml +0 -25
- obtrace_sdk_python-1.0.0/src/obtrace_sdk/__init__.py +0 -12
- obtrace_sdk_python-1.0.0/src/obtrace_sdk/client.py +0 -159
- obtrace_sdk_python-1.0.0/src/obtrace_sdk/context.py +0 -31
- obtrace_sdk_python-1.0.0/src/obtrace_sdk/http.py +0 -159
- obtrace_sdk_python-1.0.0/src/obtrace_sdk/otlp.py +0 -161
- obtrace_sdk_python-1.0.0/src/obtrace_sdk_python.egg-info/requires.txt +0 -13
- obtrace_sdk_python-1.0.0/tests/test_client.py +0 -32
- obtrace_sdk_python-1.0.0/tests/test_context.py +0 -21
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/setup.cfg +0 -0
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk/semantic_metrics.py +0 -0
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk_python.egg-info/dependency_links.txt +0 -0
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk_python.egg-info/top_level.txt +0 -0
- {obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/tests/test_semantic_metrics.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Obtrace
|
|
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.
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: obtrace-sdk-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Obtrace Python SDK
|
|
5
5
|
Author: Obtrace
|
|
6
|
-
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
Requires-Dist:
|
|
11
|
-
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
|
|
17
|
-
Requires-Dist:
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
11
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
12
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
13
|
+
Requires-Dist: opentelemetry-instrumentation-requests
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-httpx
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-urllib
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-flask
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-django
|
|
19
|
+
Requires-Dist: opentelemetry-instrumentation-logging
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg2
|
|
21
|
+
Requires-Dist: opentelemetry-instrumentation-redis
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation-celery
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation-grpc
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
18
28
|
|
|
19
29
|
# obtrace-sdk-python
|
|
20
30
|
|
|
@@ -34,7 +44,7 @@ SDK is thin/dumb.
|
|
|
34
44
|
## Install
|
|
35
45
|
|
|
36
46
|
```bash
|
|
37
|
-
pip install
|
|
47
|
+
pip install obtrace-sdk-python
|
|
38
48
|
```
|
|
39
49
|
|
|
40
50
|
## Configuration
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "obtrace-sdk-python"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Obtrace Python SDK"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
authors = [{ name = "Obtrace" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"opentelemetry-sdk>=1.20.0",
|
|
15
|
+
"opentelemetry-api>=1.20.0",
|
|
16
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20.0",
|
|
17
|
+
"opentelemetry-instrumentation-requests",
|
|
18
|
+
"opentelemetry-instrumentation-httpx",
|
|
19
|
+
"opentelemetry-instrumentation-urllib",
|
|
20
|
+
"opentelemetry-instrumentation-flask",
|
|
21
|
+
"opentelemetry-instrumentation-fastapi",
|
|
22
|
+
"opentelemetry-instrumentation-django",
|
|
23
|
+
"opentelemetry-instrumentation-logging",
|
|
24
|
+
"opentelemetry-instrumentation-psycopg2",
|
|
25
|
+
"opentelemetry-instrumentation-redis",
|
|
26
|
+
"opentelemetry-instrumentation-sqlalchemy",
|
|
27
|
+
"opentelemetry-instrumentation-celery",
|
|
28
|
+
"opentelemetry-instrumentation-grpc",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=8.0"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = {"" = "src"}
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .client import ObtraceClient
|
|
2
|
+
from .logging_handler import ObtraceLoggingHandler, install_logging_hook
|
|
3
|
+
from .otel_setup import setup_otel, OtelProviders
|
|
4
|
+
from .semantic_metrics import SemanticMetrics, is_semantic_metric
|
|
5
|
+
from .types import ObtraceConfig, SDKContext
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ObtraceClient",
|
|
9
|
+
"ObtraceConfig",
|
|
10
|
+
"ObtraceLoggingHandler",
|
|
11
|
+
"OtelProviders",
|
|
12
|
+
"SDKContext",
|
|
13
|
+
"SemanticMetrics",
|
|
14
|
+
"install_logging_hook",
|
|
15
|
+
"is_semantic_metric",
|
|
16
|
+
"setup_otel",
|
|
17
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Any, Dict, Iterator, Optional
|
|
6
|
+
|
|
7
|
+
from opentelemetry._logs.severity import SeverityNumber
|
|
8
|
+
from opentelemetry.instrumentation.logging.handler import LoggingHandler
|
|
9
|
+
from opentelemetry.trace import StatusCode
|
|
10
|
+
|
|
11
|
+
from .otel_setup import setup_otel
|
|
12
|
+
from .semantic_metrics import is_semantic_metric
|
|
13
|
+
from .types import ObtraceConfig, SDKContext
|
|
14
|
+
|
|
15
|
+
_SDK_SCOPE = "obtrace-sdk-python"
|
|
16
|
+
_logger = logging.getLogger("obtrace")
|
|
17
|
+
_initialized = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ObtraceClient:
|
|
21
|
+
def __init__(self, cfg: ObtraceConfig):
|
|
22
|
+
global _initialized
|
|
23
|
+
if not cfg.api_key or not cfg.ingest_base_url or not cfg.service_name:
|
|
24
|
+
raise ValueError("api_key, ingest_base_url and service_name are required")
|
|
25
|
+
if _initialized:
|
|
26
|
+
_logger.warning("obtrace: ObtraceClient already initialized, creating duplicate instance")
|
|
27
|
+
_initialized = True
|
|
28
|
+
self.cfg = cfg
|
|
29
|
+
self._providers = setup_otel(cfg)
|
|
30
|
+
self._tracer = self._providers.tracer_provider.get_tracer(_SDK_SCOPE)
|
|
31
|
+
self._meter = self._providers.meter_provider.get_meter(_SDK_SCOPE)
|
|
32
|
+
self._logger = self._providers.logger_provider.get_logger(_SDK_SCOPE)
|
|
33
|
+
self._counters: Dict[str, Any] = {}
|
|
34
|
+
self._histograms: Dict[str, Any] = {}
|
|
35
|
+
self._otel_logging_handler = LoggingHandler(
|
|
36
|
+
level=logging.DEBUG,
|
|
37
|
+
logger_provider=self._providers.logger_provider,
|
|
38
|
+
)
|
|
39
|
+
logging.root.addHandler(self._otel_logging_handler)
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> ObtraceClient:
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *_: Any) -> None:
|
|
45
|
+
self.shutdown()
|
|
46
|
+
|
|
47
|
+
def log(self, level: str, message: str, context: Optional[SDKContext] = None) -> None:
|
|
48
|
+
severity = _level_to_severity(level)
|
|
49
|
+
attrs: Dict[str, Any] = {}
|
|
50
|
+
if context:
|
|
51
|
+
if context.trace_id:
|
|
52
|
+
attrs["obtrace.trace_id"] = context.trace_id
|
|
53
|
+
if context.span_id:
|
|
54
|
+
attrs["obtrace.span_id"] = context.span_id
|
|
55
|
+
if context.session_id:
|
|
56
|
+
attrs["obtrace.session_id"] = context.session_id
|
|
57
|
+
if context.route_template:
|
|
58
|
+
attrs["obtrace.route_template"] = context.route_template
|
|
59
|
+
if context.endpoint:
|
|
60
|
+
attrs["obtrace.endpoint"] = context.endpoint
|
|
61
|
+
if context.method:
|
|
62
|
+
attrs["obtrace.method"] = context.method
|
|
63
|
+
if context.status_code is not None:
|
|
64
|
+
attrs["obtrace.status_code"] = context.status_code
|
|
65
|
+
for k, v in context.attrs.items():
|
|
66
|
+
attrs[f"obtrace.attr.{k}"] = v
|
|
67
|
+
self._logger.emit(
|
|
68
|
+
body=message,
|
|
69
|
+
severity_text=level.upper(),
|
|
70
|
+
severity_number=_severity_number_enum(severity),
|
|
71
|
+
attributes=attrs if attrs else None,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def metric(self, name: str, value: float, unit: str = "1", context: Optional[SDKContext] = None) -> None:
|
|
75
|
+
if self.cfg.validate_semantic_metrics and self.cfg.debug and not is_semantic_metric(name):
|
|
76
|
+
print(f"[obtrace-sdk-python] non-canonical metric name: {name}")
|
|
77
|
+
attrs = {}
|
|
78
|
+
if context:
|
|
79
|
+
attrs = dict(context.attrs)
|
|
80
|
+
key = f"{name}:{unit}"
|
|
81
|
+
if key not in self._counters:
|
|
82
|
+
self._counters[key] = self._meter.create_gauge(name, unit=unit)
|
|
83
|
+
self._counters[key].set(value, attributes=attrs)
|
|
84
|
+
|
|
85
|
+
def span(
|
|
86
|
+
self,
|
|
87
|
+
name: str,
|
|
88
|
+
trace_id: Optional[str] = None,
|
|
89
|
+
span_id: Optional[str] = None,
|
|
90
|
+
start_unix_nano: Optional[str] = None,
|
|
91
|
+
end_unix_nano: Optional[str] = None,
|
|
92
|
+
status_code: Optional[int] = None,
|
|
93
|
+
status_message: str = "",
|
|
94
|
+
attrs: Optional[Dict[str, Any]] = None,
|
|
95
|
+
) -> Dict[str, str]:
|
|
96
|
+
otel_span = self._tracer.start_span(name, attributes=attrs or {})
|
|
97
|
+
if status_code is not None and status_code >= 400:
|
|
98
|
+
otel_span.set_status(StatusCode.ERROR, status_message)
|
|
99
|
+
else:
|
|
100
|
+
otel_span.set_status(StatusCode.OK)
|
|
101
|
+
otel_span.end()
|
|
102
|
+
ctx = otel_span.get_span_context()
|
|
103
|
+
return {
|
|
104
|
+
"trace_id": format(ctx.trace_id, "032x"),
|
|
105
|
+
"span_id": format(ctx.span_id, "016x"),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@contextmanager
|
|
109
|
+
def start_span(self, name: str, attrs: Optional[Dict[str, Any]] = None) -> Iterator[Any]:
|
|
110
|
+
with self._tracer.start_as_current_span(name, attributes=attrs or {}) as otel_span:
|
|
111
|
+
yield otel_span
|
|
112
|
+
|
|
113
|
+
def capture_error(self, error: Exception, attrs: Optional[Dict[str, Any]] = None) -> None:
|
|
114
|
+
with self._tracer.start_as_current_span("exception") as otel_span:
|
|
115
|
+
otel_span.set_status(StatusCode.ERROR, str(error))
|
|
116
|
+
otel_span.record_exception(error, attributes=attrs or {})
|
|
117
|
+
|
|
118
|
+
def inject_propagation(
|
|
119
|
+
self,
|
|
120
|
+
headers: Optional[Dict[str, str]] = None,
|
|
121
|
+
trace_id: Optional[str] = None,
|
|
122
|
+
span_id: Optional[str] = None,
|
|
123
|
+
session_id: Optional[str] = None,
|
|
124
|
+
) -> Dict[str, str]:
|
|
125
|
+
from opentelemetry.propagate import inject
|
|
126
|
+
out = dict(headers or {})
|
|
127
|
+
inject(out)
|
|
128
|
+
if session_id:
|
|
129
|
+
out.setdefault("x-obtrace-session-id", session_id)
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
def flush(self) -> None:
|
|
133
|
+
self._providers.tracer_provider.force_flush()
|
|
134
|
+
self._providers.meter_provider.force_flush()
|
|
135
|
+
self._providers.logger_provider.force_flush()
|
|
136
|
+
|
|
137
|
+
def shutdown(self) -> None:
|
|
138
|
+
global _initialized
|
|
139
|
+
logging.root.removeHandler(self._otel_logging_handler)
|
|
140
|
+
self._providers.tracer_provider.shutdown()
|
|
141
|
+
self._providers.meter_provider.shutdown()
|
|
142
|
+
self._providers.logger_provider.shutdown()
|
|
143
|
+
_initialized = False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _level_to_severity(level: str) -> int:
|
|
147
|
+
mapping = {
|
|
148
|
+
"debug": 5,
|
|
149
|
+
"info": 9,
|
|
150
|
+
"warn": 13,
|
|
151
|
+
"warning": 13,
|
|
152
|
+
"error": 17,
|
|
153
|
+
"fatal": 21,
|
|
154
|
+
"critical": 21,
|
|
155
|
+
}
|
|
156
|
+
return mapping.get(level.lower(), 9)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_SEVERITY_MAP = {
|
|
160
|
+
5: SeverityNumber.DEBUG,
|
|
161
|
+
9: SeverityNumber.INFO,
|
|
162
|
+
13: SeverityNumber.WARN,
|
|
163
|
+
17: SeverityNumber.ERROR,
|
|
164
|
+
21: SeverityNumber.FATAL,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _severity_number_enum(num: int) -> SeverityNumber:
|
|
169
|
+
return _SEVERITY_MAP.get(num, SeverityNumber.INFO)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from opentelemetry.instrumentation.logging.handler import LoggingHandler
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .client import ObtraceClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ObtraceLoggingHandler(LoggingHandler):
|
|
13
|
+
def __init__(self, client: ObtraceClient, level: int = logging.DEBUG):
|
|
14
|
+
super().__init__(
|
|
15
|
+
level=level,
|
|
16
|
+
logger_provider=client._providers.logger_provider,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def install_logging_hook(client: ObtraceClient, level: int = logging.DEBUG) -> ObtraceLoggingHandler:
|
|
21
|
+
handler = ObtraceLoggingHandler(client, level)
|
|
22
|
+
logging.root.addHandler(handler)
|
|
23
|
+
return handler
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("obtrace")
|
|
7
|
+
|
|
8
|
+
from opentelemetry import metrics, trace
|
|
9
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
10
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
12
|
+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
13
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
14
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
15
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
16
|
+
from opentelemetry.sdk.resources import Resource
|
|
17
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
18
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
19
|
+
|
|
20
|
+
from .types import ObtraceConfig
|
|
21
|
+
|
|
22
|
+
_INSTRUMENTATION_MODULES = [
|
|
23
|
+
("opentelemetry.instrumentation.requests", "RequestsInstrumentor"),
|
|
24
|
+
("opentelemetry.instrumentation.httpx", "HTTPXClientInstrumentor"),
|
|
25
|
+
("opentelemetry.instrumentation.urllib", "URLLibInstrumentor"),
|
|
26
|
+
("opentelemetry.instrumentation.flask", "FlaskInstrumentor"),
|
|
27
|
+
("opentelemetry.instrumentation.fastapi", "FastAPIInstrumentor"),
|
|
28
|
+
("opentelemetry.instrumentation.django", "DjangoInstrumentor"),
|
|
29
|
+
("opentelemetry.instrumentation.logging", "LoggingInstrumentor"),
|
|
30
|
+
("opentelemetry.instrumentation.psycopg2", "Psycopg2Instrumentor"),
|
|
31
|
+
("opentelemetry.instrumentation.redis", "RedisInstrumentor"),
|
|
32
|
+
("opentelemetry.instrumentation.sqlalchemy", "SQLAlchemyInstrumentor"),
|
|
33
|
+
("opentelemetry.instrumentation.celery", "CeleryInstrumentor"),
|
|
34
|
+
("opentelemetry.instrumentation.grpc", "GrpcInstrumentorClient"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OtelProviders:
|
|
39
|
+
__slots__ = ("tracer_provider", "meter_provider", "logger_provider")
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
tracer_provider: TracerProvider,
|
|
44
|
+
meter_provider: MeterProvider,
|
|
45
|
+
logger_provider: LoggerProvider,
|
|
46
|
+
):
|
|
47
|
+
self.tracer_provider = tracer_provider
|
|
48
|
+
self.meter_provider = meter_provider
|
|
49
|
+
self.logger_provider = logger_provider
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def setup_otel(cfg: ObtraceConfig) -> OtelProviders:
|
|
53
|
+
resource_attrs: dict[str, Any] = {
|
|
54
|
+
"service.name": cfg.service_name,
|
|
55
|
+
"service.version": cfg.service_version,
|
|
56
|
+
"deployment.environment": cfg.env or "dev",
|
|
57
|
+
"runtime.name": "python",
|
|
58
|
+
}
|
|
59
|
+
if cfg.tenant_id:
|
|
60
|
+
resource_attrs["obtrace.tenant_id"] = cfg.tenant_id
|
|
61
|
+
if cfg.project_id:
|
|
62
|
+
resource_attrs["obtrace.project_id"] = cfg.project_id
|
|
63
|
+
if cfg.app_id:
|
|
64
|
+
resource_attrs["obtrace.app_id"] = cfg.app_id
|
|
65
|
+
if cfg.env:
|
|
66
|
+
resource_attrs["obtrace.env"] = cfg.env
|
|
67
|
+
|
|
68
|
+
resource = Resource.create(resource_attrs)
|
|
69
|
+
base_url = cfg.ingest_base_url.rstrip("/")
|
|
70
|
+
headers = {
|
|
71
|
+
**cfg.default_headers,
|
|
72
|
+
"Authorization": f"Bearer {cfg.api_key}",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
76
|
+
tracer_provider.add_span_processor(
|
|
77
|
+
BatchSpanProcessor(
|
|
78
|
+
OTLPSpanExporter(
|
|
79
|
+
endpoint=f"{base_url}/otlp/v1/traces",
|
|
80
|
+
headers=headers,
|
|
81
|
+
timeout=int(cfg.request_timeout_sec),
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
try:
|
|
86
|
+
trace.set_tracer_provider(tracer_provider)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
meter_provider = MeterProvider(
|
|
91
|
+
resource=resource,
|
|
92
|
+
metric_readers=[
|
|
93
|
+
PeriodicExportingMetricReader(
|
|
94
|
+
OTLPMetricExporter(
|
|
95
|
+
endpoint=f"{base_url}/otlp/v1/metrics",
|
|
96
|
+
headers=headers,
|
|
97
|
+
timeout=int(cfg.request_timeout_sec),
|
|
98
|
+
),
|
|
99
|
+
export_interval_millis=60000,
|
|
100
|
+
)
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
try:
|
|
104
|
+
metrics.set_meter_provider(meter_provider)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
logger_provider = LoggerProvider(resource=resource)
|
|
109
|
+
logger_provider.add_log_record_processor(
|
|
110
|
+
BatchLogRecordProcessor(
|
|
111
|
+
OTLPLogExporter(
|
|
112
|
+
endpoint=f"{base_url}/otlp/v1/logs",
|
|
113
|
+
headers=headers,
|
|
114
|
+
timeout=int(cfg.request_timeout_sec),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
providers = OtelProviders(
|
|
120
|
+
tracer_provider=tracer_provider,
|
|
121
|
+
meter_provider=meter_provider,
|
|
122
|
+
logger_provider=logger_provider,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if cfg.auto_instrument_http:
|
|
126
|
+
import threading
|
|
127
|
+
threading.Thread(target=_auto_instrument, daemon=True).start()
|
|
128
|
+
|
|
129
|
+
return providers
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _auto_instrument() -> None:
|
|
133
|
+
import importlib
|
|
134
|
+
|
|
135
|
+
for module_path, class_name in _INSTRUMENTATION_MODULES:
|
|
136
|
+
try:
|
|
137
|
+
mod = importlib.import_module(module_path)
|
|
138
|
+
instrumentor = getattr(mod, class_name)()
|
|
139
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
140
|
+
instrumentor.instrument()
|
|
141
|
+
except (ImportError, Exception) as e:
|
|
142
|
+
logger.debug("obtrace: skipped %s: %s", module_path, e)
|
|
@@ -15,9 +15,9 @@ class ObtraceConfig:
|
|
|
15
15
|
app_id: Optional[str] = None
|
|
16
16
|
env: Optional[str] = None
|
|
17
17
|
request_timeout_sec: float = 5.0
|
|
18
|
-
max_queue_size: int = 1000
|
|
19
18
|
validate_semantic_metrics: bool = False
|
|
20
19
|
debug: bool = False
|
|
20
|
+
auto_instrument_http: bool = True
|
|
21
21
|
default_headers: Dict[str, str] = field(default_factory=dict)
|
|
22
22
|
|
|
23
23
|
|
{obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk_python.egg-info/PKG-INFO
RENAMED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: obtrace-sdk-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Obtrace Python SDK
|
|
5
5
|
Author: Obtrace
|
|
6
|
-
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
Requires-Dist:
|
|
11
|
-
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
|
|
17
|
-
Requires-Dist:
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
11
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
12
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
13
|
+
Requires-Dist: opentelemetry-instrumentation-requests
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-httpx
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-urllib
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-flask
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-django
|
|
19
|
+
Requires-Dist: opentelemetry-instrumentation-logging
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg2
|
|
21
|
+
Requires-Dist: opentelemetry-instrumentation-redis
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation-celery
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation-grpc
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
18
28
|
|
|
19
29
|
# obtrace-sdk-python
|
|
20
30
|
|
|
@@ -34,7 +44,7 @@ SDK is thin/dumb.
|
|
|
34
44
|
## Install
|
|
35
45
|
|
|
36
46
|
```bash
|
|
37
|
-
pip install
|
|
47
|
+
pip install obtrace-sdk-python
|
|
38
48
|
```
|
|
39
49
|
|
|
40
50
|
## Configuration
|
{obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk_python.egg-info/SOURCES.txt
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
src/obtrace_sdk/__init__.py
|
|
4
5
|
src/obtrace_sdk/client.py
|
|
5
|
-
src/obtrace_sdk/
|
|
6
|
-
src/obtrace_sdk/
|
|
7
|
-
src/obtrace_sdk/otlp.py
|
|
6
|
+
src/obtrace_sdk/logging_handler.py
|
|
7
|
+
src/obtrace_sdk/otel_setup.py
|
|
8
8
|
src/obtrace_sdk/semantic_metrics.py
|
|
9
9
|
src/obtrace_sdk/types.py
|
|
10
10
|
src/obtrace_sdk_python.egg-info/PKG-INFO
|
|
@@ -13,5 +13,4 @@ src/obtrace_sdk_python.egg-info/dependency_links.txt
|
|
|
13
13
|
src/obtrace_sdk_python.egg-info/requires.txt
|
|
14
14
|
src/obtrace_sdk_python.egg-info/top_level.txt
|
|
15
15
|
tests/test_client.py
|
|
16
|
-
tests/test_context.py
|
|
17
16
|
tests/test_semantic_metrics.py
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
opentelemetry-sdk>=1.20.0
|
|
2
|
+
opentelemetry-api>=1.20.0
|
|
3
|
+
opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
4
|
+
opentelemetry-instrumentation-requests
|
|
5
|
+
opentelemetry-instrumentation-httpx
|
|
6
|
+
opentelemetry-instrumentation-urllib
|
|
7
|
+
opentelemetry-instrumentation-flask
|
|
8
|
+
opentelemetry-instrumentation-fastapi
|
|
9
|
+
opentelemetry-instrumentation-django
|
|
10
|
+
opentelemetry-instrumentation-logging
|
|
11
|
+
opentelemetry-instrumentation-psycopg2
|
|
12
|
+
opentelemetry-instrumentation-redis
|
|
13
|
+
opentelemetry-instrumentation-sqlalchemy
|
|
14
|
+
opentelemetry-instrumentation-celery
|
|
15
|
+
opentelemetry-instrumentation-grpc
|
|
16
|
+
|
|
17
|
+
[dev]
|
|
18
|
+
pytest>=8.0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from opentelemetry.sdk._logs import LoggerProvider
|
|
4
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
5
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
6
|
+
|
|
7
|
+
from obtrace_sdk.client import ObtraceClient
|
|
8
|
+
from obtrace_sdk.types import ObtraceConfig, SDKContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_client(**overrides):
|
|
12
|
+
defaults = dict(
|
|
13
|
+
api_key="devkey",
|
|
14
|
+
ingest_base_url="https://ingest.obtrace.ai",
|
|
15
|
+
service_name="py-test",
|
|
16
|
+
auto_instrument_http=False,
|
|
17
|
+
)
|
|
18
|
+
defaults.update(overrides)
|
|
19
|
+
return ObtraceClient(ObtraceConfig(**defaults))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClientTests(unittest.TestCase):
|
|
23
|
+
def test_creates_otel_providers(self):
|
|
24
|
+
c = _make_client()
|
|
25
|
+
self.assertIsInstance(c._providers.tracer_provider, TracerProvider)
|
|
26
|
+
self.assertIsInstance(c._providers.meter_provider, MeterProvider)
|
|
27
|
+
self.assertIsInstance(c._providers.logger_provider, LoggerProvider)
|
|
28
|
+
c.shutdown()
|
|
29
|
+
|
|
30
|
+
def test_log_does_not_raise(self):
|
|
31
|
+
c = _make_client()
|
|
32
|
+
c.log("info", "hello")
|
|
33
|
+
c.log("error", "bad thing", SDKContext(trace_id="a" * 32, span_id="b" * 16))
|
|
34
|
+
c.shutdown()
|
|
35
|
+
|
|
36
|
+
def test_metric_does_not_raise(self):
|
|
37
|
+
c = _make_client()
|
|
38
|
+
c.metric("m", 1.0)
|
|
39
|
+
c.metric("m2", 42.0, unit="ms", context=SDKContext(attrs={"env": "test"}))
|
|
40
|
+
c.shutdown()
|
|
41
|
+
|
|
42
|
+
def test_span_returns_trace_and_span_id(self):
|
|
43
|
+
c = _make_client()
|
|
44
|
+
result = c.span("test-span", attrs={"key": "value"})
|
|
45
|
+
self.assertIn("trace_id", result)
|
|
46
|
+
self.assertIn("span_id", result)
|
|
47
|
+
self.assertEqual(len(result["trace_id"]), 32)
|
|
48
|
+
self.assertEqual(len(result["span_id"]), 16)
|
|
49
|
+
c.shutdown()
|
|
50
|
+
|
|
51
|
+
def test_span_error_status(self):
|
|
52
|
+
c = _make_client()
|
|
53
|
+
result = c.span("error-span", status_code=500, status_message="internal error")
|
|
54
|
+
self.assertIn("trace_id", result)
|
|
55
|
+
c.shutdown()
|
|
56
|
+
|
|
57
|
+
def test_capture_error(self):
|
|
58
|
+
c = _make_client()
|
|
59
|
+
c.capture_error(ValueError("test error"), attrs={"component": "db"})
|
|
60
|
+
c.shutdown()
|
|
61
|
+
|
|
62
|
+
def test_context_manager(self):
|
|
63
|
+
with _make_client() as c:
|
|
64
|
+
c.log("info", "inside context")
|
|
65
|
+
|
|
66
|
+
def test_start_span_context_manager(self):
|
|
67
|
+
c = _make_client()
|
|
68
|
+
with c.start_span("my-operation", attrs={"step": "1"}) as span:
|
|
69
|
+
span.set_attribute("result", "ok")
|
|
70
|
+
c.shutdown()
|
|
71
|
+
|
|
72
|
+
def test_inject_propagation(self):
|
|
73
|
+
c = _make_client()
|
|
74
|
+
headers = c.inject_propagation(session_id="sess-123")
|
|
75
|
+
self.assertIn("x-obtrace-session-id", headers)
|
|
76
|
+
self.assertEqual(headers["x-obtrace-session-id"], "sess-123")
|
|
77
|
+
c.shutdown()
|
|
78
|
+
|
|
79
|
+
def test_flush_does_not_raise(self):
|
|
80
|
+
c = _make_client()
|
|
81
|
+
c.log("info", "pre-flush")
|
|
82
|
+
c.flush()
|
|
83
|
+
c.shutdown()
|
|
84
|
+
|
|
85
|
+
def test_validation_error_on_missing_fields(self):
|
|
86
|
+
with self.assertRaises(ValueError):
|
|
87
|
+
ObtraceClient(ObtraceConfig(api_key="", ingest_base_url="http://x", service_name="svc"))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
unittest.main()
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=69", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "obtrace-sdk-python"
|
|
7
|
-
version = "1.0.0"
|
|
8
|
-
description = "Obtrace Python SDK"
|
|
9
|
-
classifiers = ["License :: OSI Approved :: MIT License"]
|
|
10
|
-
requires-python = ">=3.10"
|
|
11
|
-
readme = "README.md"
|
|
12
|
-
authors = [{ name = "Obtrace" }]
|
|
13
|
-
dependencies = []
|
|
14
|
-
|
|
15
|
-
[project.optional-dependencies]
|
|
16
|
-
requests = ["requests>=2.31.0"]
|
|
17
|
-
httpx = ["httpx>=0.27.0"]
|
|
18
|
-
fastapi = ["fastapi>=0.112.0", "starlette>=0.37.0"]
|
|
19
|
-
flask = ["flask>=3.0.0"]
|
|
20
|
-
|
|
21
|
-
[tool.setuptools]
|
|
22
|
-
package-dir = {"" = "src"}
|
|
23
|
-
|
|
24
|
-
[tool.setuptools.packages.find]
|
|
25
|
-
where = ["src"]
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from .client import ObtraceClient, ObtraceConfig
|
|
2
|
-
from .context import create_traceparent, ensure_propagation_headers
|
|
3
|
-
from .semantic_metrics import SemanticMetrics, is_semantic_metric
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"ObtraceClient",
|
|
7
|
-
"ObtraceConfig",
|
|
8
|
-
"SemanticMetrics",
|
|
9
|
-
"is_semantic_metric",
|
|
10
|
-
"create_traceparent",
|
|
11
|
-
"ensure_propagation_headers",
|
|
12
|
-
]
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import atexit
|
|
4
|
-
import json
|
|
5
|
-
import threading
|
|
6
|
-
import time
|
|
7
|
-
import urllib.error
|
|
8
|
-
import urllib.request
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, Dict, List, Optional
|
|
11
|
-
|
|
12
|
-
from .context import ensure_propagation_headers, random_hex
|
|
13
|
-
from .otlp import build_logs_payload, build_metric_payload, build_span_payload
|
|
14
|
-
from .semantic_metrics import is_semantic_metric
|
|
15
|
-
from .types import ObtraceConfig, SDKContext
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@dataclass(slots=True)
|
|
19
|
-
class _Queued:
|
|
20
|
-
endpoint: str
|
|
21
|
-
payload: Dict[str, Any]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ObtraceClient:
|
|
25
|
-
def __init__(self, cfg: ObtraceConfig):
|
|
26
|
-
if not cfg.api_key or not cfg.ingest_base_url or not cfg.service_name:
|
|
27
|
-
raise ValueError("api_key, ingest_base_url and service_name are required")
|
|
28
|
-
self.cfg = cfg
|
|
29
|
-
self._queue: List[_Queued] = []
|
|
30
|
-
self._lock = threading.Lock()
|
|
31
|
-
self._circuit_failures = 0
|
|
32
|
-
self._circuit_open_until = 0.0
|
|
33
|
-
atexit.register(self.flush)
|
|
34
|
-
|
|
35
|
-
def __enter__(self) -> ObtraceClient:
|
|
36
|
-
return self
|
|
37
|
-
|
|
38
|
-
def __exit__(self, *_: Any) -> None:
|
|
39
|
-
self.flush()
|
|
40
|
-
|
|
41
|
-
@staticmethod
|
|
42
|
-
def _truncate(s: str, max_len: int) -> str:
|
|
43
|
-
if len(s) <= max_len:
|
|
44
|
-
return s
|
|
45
|
-
return s[:max_len] + "...[truncated]"
|
|
46
|
-
|
|
47
|
-
def log(self, level: str, message: str, context: Optional[SDKContext] = None) -> None:
|
|
48
|
-
self._enqueue("/otlp/v1/logs", build_logs_payload(self.cfg, level, self._truncate(message, 32768), context))
|
|
49
|
-
|
|
50
|
-
def metric(self, name: str, value: float, unit: str = "1", context: Optional[SDKContext] = None) -> None:
|
|
51
|
-
if self.cfg.validate_semantic_metrics and self.cfg.debug and not is_semantic_metric(name):
|
|
52
|
-
print(f"[obtrace-sdk-python] non-canonical metric name: {name}")
|
|
53
|
-
self._enqueue("/otlp/v1/metrics", build_metric_payload(self.cfg, self._truncate(name, 1024), value, unit, context))
|
|
54
|
-
|
|
55
|
-
def span(
|
|
56
|
-
self,
|
|
57
|
-
name: str,
|
|
58
|
-
trace_id: Optional[str] = None,
|
|
59
|
-
span_id: Optional[str] = None,
|
|
60
|
-
start_unix_nano: Optional[str] = None,
|
|
61
|
-
end_unix_nano: Optional[str] = None,
|
|
62
|
-
status_code: Optional[int] = None,
|
|
63
|
-
status_message: str = "",
|
|
64
|
-
attrs: Optional[Dict[str, Any]] = None,
|
|
65
|
-
) -> Dict[str, str]:
|
|
66
|
-
t = trace_id if trace_id and len(trace_id) == 32 else random_hex(16)
|
|
67
|
-
s = span_id if span_id and len(span_id) == 16 else random_hex(8)
|
|
68
|
-
start = start_unix_nano or str(int(time.time() * 1_000_000_000))
|
|
69
|
-
end = end_unix_nano or str(int(time.time() * 1_000_000_000))
|
|
70
|
-
|
|
71
|
-
truncated_name = self._truncate(name, 32768)
|
|
72
|
-
if attrs:
|
|
73
|
-
attrs = {k: self._truncate(v, 4096) if isinstance(v, str) else v for k, v in attrs.items()}
|
|
74
|
-
|
|
75
|
-
self._enqueue(
|
|
76
|
-
"/otlp/v1/traces",
|
|
77
|
-
build_span_payload(self.cfg, truncated_name, t, s, start, end, status_code, status_message, attrs),
|
|
78
|
-
)
|
|
79
|
-
return {"trace_id": t, "span_id": s}
|
|
80
|
-
|
|
81
|
-
def inject_propagation(
|
|
82
|
-
self,
|
|
83
|
-
headers: Optional[Dict[str, str]] = None,
|
|
84
|
-
trace_id: Optional[str] = None,
|
|
85
|
-
span_id: Optional[str] = None,
|
|
86
|
-
session_id: Optional[str] = None,
|
|
87
|
-
) -> Dict[str, str]:
|
|
88
|
-
return ensure_propagation_headers(headers, trace_id, span_id, session_id)
|
|
89
|
-
|
|
90
|
-
def flush(self) -> None:
|
|
91
|
-
with self._lock:
|
|
92
|
-
now = time.time()
|
|
93
|
-
if now < self._circuit_open_until:
|
|
94
|
-
return
|
|
95
|
-
half_open = self._circuit_failures >= 5
|
|
96
|
-
if half_open:
|
|
97
|
-
batch = self._queue[:1]
|
|
98
|
-
self._queue = self._queue[1:]
|
|
99
|
-
else:
|
|
100
|
-
batch = list(self._queue)
|
|
101
|
-
self._queue.clear()
|
|
102
|
-
|
|
103
|
-
for item in batch:
|
|
104
|
-
try:
|
|
105
|
-
self._send(item)
|
|
106
|
-
with self._lock:
|
|
107
|
-
if self._circuit_failures > 0:
|
|
108
|
-
if self.cfg.debug:
|
|
109
|
-
print("[obtrace-sdk-python] circuit breaker closed")
|
|
110
|
-
self._circuit_failures = 0
|
|
111
|
-
self._circuit_open_until = 0.0
|
|
112
|
-
except Exception: # noqa: BLE001
|
|
113
|
-
with self._lock:
|
|
114
|
-
self._circuit_failures += 1
|
|
115
|
-
if self._circuit_failures >= 5:
|
|
116
|
-
self._circuit_open_until = time.time() + 30.0
|
|
117
|
-
if self.cfg.debug:
|
|
118
|
-
print("[obtrace-sdk-python] circuit breaker opened")
|
|
119
|
-
if self.cfg.debug:
|
|
120
|
-
import traceback
|
|
121
|
-
traceback.print_exc()
|
|
122
|
-
|
|
123
|
-
def shutdown(self) -> None:
|
|
124
|
-
self.flush()
|
|
125
|
-
|
|
126
|
-
def _enqueue(self, endpoint: str, payload: Dict[str, Any]) -> None:
|
|
127
|
-
with self._lock:
|
|
128
|
-
if len(self._queue) >= self.cfg.max_queue_size:
|
|
129
|
-
if self.cfg.debug:
|
|
130
|
-
print(f"[obtrace-sdk-python] queue full, dropping oldest item")
|
|
131
|
-
self._queue.pop(0)
|
|
132
|
-
self._queue.append(_Queued(endpoint=endpoint, payload=payload))
|
|
133
|
-
|
|
134
|
-
def _send(self, item: _Queued) -> None:
|
|
135
|
-
try:
|
|
136
|
-
body = json.dumps(item.payload).encode("utf-8")
|
|
137
|
-
except (TypeError, ValueError):
|
|
138
|
-
if self.cfg.debug:
|
|
139
|
-
print(f"[obtrace-sdk-python] failed to serialize payload for {item.endpoint}")
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
req = urllib.request.Request(
|
|
143
|
-
url=f"{self.cfg.ingest_base_url.rstrip('/')}{item.endpoint}",
|
|
144
|
-
method="POST",
|
|
145
|
-
data=body,
|
|
146
|
-
headers={
|
|
147
|
-
**self.cfg.default_headers,
|
|
148
|
-
"Authorization": f"Bearer {self.cfg.api_key}",
|
|
149
|
-
"Content-Type": "application/json",
|
|
150
|
-
},
|
|
151
|
-
)
|
|
152
|
-
try:
|
|
153
|
-
with urllib.request.urlopen(req, timeout=self.cfg.request_timeout_sec) as res:
|
|
154
|
-
code = int(getattr(res, "status", 200))
|
|
155
|
-
if code >= 300 and self.cfg.debug:
|
|
156
|
-
print(f"[obtrace-sdk-python] status={code} endpoint={item.endpoint}")
|
|
157
|
-
except (urllib.error.URLError, TypeError, ValueError, OSError) as exc:
|
|
158
|
-
if self.cfg.debug:
|
|
159
|
-
print(f"[obtrace-sdk-python] send failed endpoint={item.endpoint} err={exc}")
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import secrets
|
|
4
|
-
from typing import Dict, Optional
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def random_hex(nbytes: int) -> str:
|
|
8
|
-
return secrets.token_hex(nbytes)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def create_traceparent(trace_id: Optional[str] = None, span_id: Optional[str] = None) -> str:
|
|
12
|
-
t = trace_id if trace_id and len(trace_id) == 32 else random_hex(16)
|
|
13
|
-
s = span_id if span_id and len(span_id) == 16 else random_hex(8)
|
|
14
|
-
return f"00-{t}-{s}-01"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def ensure_propagation_headers(
|
|
18
|
-
headers: Optional[Dict[str, str]] = None,
|
|
19
|
-
trace_id: Optional[str] = None,
|
|
20
|
-
span_id: Optional[str] = None,
|
|
21
|
-
session_id: Optional[str] = None,
|
|
22
|
-
trace_header_name: str = "traceparent",
|
|
23
|
-
session_header_name: str = "x-obtrace-session-id",
|
|
24
|
-
) -> Dict[str, str]:
|
|
25
|
-
out = dict(headers or {})
|
|
26
|
-
lower = {k.lower(): k for k in out.keys()}
|
|
27
|
-
if trace_header_name.lower() not in lower:
|
|
28
|
-
out[trace_header_name] = create_traceparent(trace_id, span_id)
|
|
29
|
-
if session_id and session_header_name.lower() not in lower:
|
|
30
|
-
out[session_header_name] = session_id
|
|
31
|
-
return out
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from typing import Any, Callable, Dict, Optional
|
|
5
|
-
|
|
6
|
-
from .client import ObtraceClient
|
|
7
|
-
from .types import SDKContext
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def instrument_requests(client: ObtraceClient, request_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
11
|
-
def wrapped(method: str, url: str, **kwargs: Any) -> Any:
|
|
12
|
-
started = time.time()
|
|
13
|
-
trace = client.span(f"http.client {method.upper()}", attrs={"http.method": method.upper(), "http.url": url})
|
|
14
|
-
|
|
15
|
-
headers = dict(kwargs.pop("headers", {}) or {})
|
|
16
|
-
headers = client.inject_propagation(headers, trace_id=trace["trace_id"], span_id=trace["span_id"])
|
|
17
|
-
kwargs["headers"] = headers
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
res = request_func(method, url, **kwargs)
|
|
21
|
-
dur_ms = int((time.time() - started) * 1000)
|
|
22
|
-
client.log(
|
|
23
|
-
"info",
|
|
24
|
-
f"requests {method.upper()} {url} -> {getattr(res, 'status_code', 200)}",
|
|
25
|
-
SDKContext(
|
|
26
|
-
trace_id=trace["trace_id"],
|
|
27
|
-
span_id=trace["span_id"],
|
|
28
|
-
method=method.upper(),
|
|
29
|
-
endpoint=url,
|
|
30
|
-
status_code=int(getattr(res, "status_code", 200)),
|
|
31
|
-
attrs={"duration_ms": dur_ms},
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
return res
|
|
35
|
-
except Exception as exc: # noqa: BLE001
|
|
36
|
-
dur_ms = int((time.time() - started) * 1000)
|
|
37
|
-
client.log(
|
|
38
|
-
"error",
|
|
39
|
-
f"requests {method.upper()} {url} failed: {exc}",
|
|
40
|
-
SDKContext(
|
|
41
|
-
trace_id=trace["trace_id"],
|
|
42
|
-
span_id=trace["span_id"],
|
|
43
|
-
method=method.upper(),
|
|
44
|
-
endpoint=url,
|
|
45
|
-
attrs={"duration_ms": dur_ms},
|
|
46
|
-
),
|
|
47
|
-
)
|
|
48
|
-
raise
|
|
49
|
-
|
|
50
|
-
return wrapped
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def instrument_httpx(client: ObtraceClient, request_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
54
|
-
async def wrapped(method: str, url: str, **kwargs: Any) -> Any:
|
|
55
|
-
started = time.time()
|
|
56
|
-
trace = client.span(f"http.client {method.upper()}", attrs={"http.method": method.upper(), "http.url": url})
|
|
57
|
-
|
|
58
|
-
headers = dict(kwargs.pop("headers", {}) or {})
|
|
59
|
-
headers = client.inject_propagation(headers, trace_id=trace["trace_id"], span_id=trace["span_id"])
|
|
60
|
-
kwargs["headers"] = headers
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
res = await request_func(method, url, **kwargs)
|
|
64
|
-
dur_ms = int((time.time() - started) * 1000)
|
|
65
|
-
client.log(
|
|
66
|
-
"info",
|
|
67
|
-
f"httpx {method.upper()} {url} -> {getattr(res, 'status_code', 200)}",
|
|
68
|
-
SDKContext(
|
|
69
|
-
trace_id=trace["trace_id"],
|
|
70
|
-
span_id=trace["span_id"],
|
|
71
|
-
method=method.upper(),
|
|
72
|
-
endpoint=url,
|
|
73
|
-
status_code=int(getattr(res, "status_code", 200)),
|
|
74
|
-
attrs={"duration_ms": dur_ms},
|
|
75
|
-
),
|
|
76
|
-
)
|
|
77
|
-
return res
|
|
78
|
-
except Exception as exc: # noqa: BLE001
|
|
79
|
-
dur_ms = int((time.time() - started) * 1000)
|
|
80
|
-
client.log(
|
|
81
|
-
"error",
|
|
82
|
-
f"httpx {method.upper()} {url} failed: {exc}",
|
|
83
|
-
SDKContext(
|
|
84
|
-
trace_id=trace["trace_id"],
|
|
85
|
-
span_id=trace["span_id"],
|
|
86
|
-
method=method.upper(),
|
|
87
|
-
endpoint=url,
|
|
88
|
-
attrs={"duration_ms": dur_ms},
|
|
89
|
-
),
|
|
90
|
-
)
|
|
91
|
-
raise
|
|
92
|
-
|
|
93
|
-
return wrapped
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def fastapi_middleware(client: ObtraceClient):
|
|
97
|
-
async def middleware(request: Any, call_next: Callable[..., Any]) -> Any:
|
|
98
|
-
started = time.time()
|
|
99
|
-
trace = client.span(
|
|
100
|
-
f"http.server {getattr(request, 'method', 'GET')}",
|
|
101
|
-
attrs={"http.method": getattr(request, "method", "GET"), "http.route": str(getattr(request, "url", ""))},
|
|
102
|
-
)
|
|
103
|
-
try:
|
|
104
|
-
response = await call_next(request)
|
|
105
|
-
dur_ms = int((time.time() - started) * 1000)
|
|
106
|
-
client.log(
|
|
107
|
-
"info",
|
|
108
|
-
f"fastapi {request.method} {request.url.path} {response.status_code}",
|
|
109
|
-
SDKContext(
|
|
110
|
-
trace_id=trace["trace_id"],
|
|
111
|
-
span_id=trace["span_id"],
|
|
112
|
-
method=request.method,
|
|
113
|
-
endpoint=request.url.path,
|
|
114
|
-
status_code=response.status_code,
|
|
115
|
-
attrs={"duration_ms": dur_ms},
|
|
116
|
-
),
|
|
117
|
-
)
|
|
118
|
-
return response
|
|
119
|
-
except Exception as exc: # noqa: BLE001
|
|
120
|
-
dur_ms = int((time.time() - started) * 1000)
|
|
121
|
-
client.log(
|
|
122
|
-
"error",
|
|
123
|
-
f"fastapi request failed: {exc}",
|
|
124
|
-
SDKContext(
|
|
125
|
-
trace_id=trace["trace_id"],
|
|
126
|
-
span_id=trace["span_id"],
|
|
127
|
-
method=getattr(request, "method", "GET"),
|
|
128
|
-
endpoint=str(getattr(request, "url", "")),
|
|
129
|
-
attrs={"duration_ms": dur_ms},
|
|
130
|
-
),
|
|
131
|
-
)
|
|
132
|
-
raise
|
|
133
|
-
|
|
134
|
-
return middleware
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def flask_before_after(client: ObtraceClient):
|
|
138
|
-
def before() -> Dict[str, Any]:
|
|
139
|
-
started = time.time()
|
|
140
|
-
trace = client.span("http.server request")
|
|
141
|
-
return {"started": started, "trace": trace}
|
|
142
|
-
|
|
143
|
-
def after(meta: Dict[str, Any], method: str, path: str, status_code: int) -> None:
|
|
144
|
-
dur_ms = int((time.time() - meta["started"]) * 1000)
|
|
145
|
-
tr = meta["trace"]
|
|
146
|
-
client.log(
|
|
147
|
-
"info",
|
|
148
|
-
f"flask {method} {path} {status_code}",
|
|
149
|
-
SDKContext(
|
|
150
|
-
trace_id=tr["trace_id"],
|
|
151
|
-
span_id=tr["span_id"],
|
|
152
|
-
method=method,
|
|
153
|
-
endpoint=path,
|
|
154
|
-
status_code=status_code,
|
|
155
|
-
attrs={"duration_ms": dur_ms},
|
|
156
|
-
),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
return before, after
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from typing import Any, Dict, Optional
|
|
5
|
-
|
|
6
|
-
from .types import ObtraceConfig, SDKContext
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _now_unix_nano_str() -> str:
|
|
10
|
-
return str(int(time.time() * 1_000_000_000))
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _attrs(attrs: Optional[Dict[str, Any]]) -> list[dict[str, Any]]:
|
|
14
|
-
out: list[dict[str, Any]] = []
|
|
15
|
-
if not attrs:
|
|
16
|
-
return out
|
|
17
|
-
for k, v in attrs.items():
|
|
18
|
-
if isinstance(v, bool):
|
|
19
|
-
val = {"boolValue": v}
|
|
20
|
-
elif isinstance(v, (int, float)):
|
|
21
|
-
val = {"doubleValue": float(v)}
|
|
22
|
-
else:
|
|
23
|
-
val = {"stringValue": str(v)}
|
|
24
|
-
out.append({"key": str(k), "value": val})
|
|
25
|
-
return out
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _resource(cfg: ObtraceConfig) -> list[dict[str, Any]]:
|
|
29
|
-
base: Dict[str, Any] = {
|
|
30
|
-
"service.name": cfg.service_name,
|
|
31
|
-
"service.version": cfg.service_version,
|
|
32
|
-
"deployment.environment": cfg.env or "dev",
|
|
33
|
-
"runtime.name": "python",
|
|
34
|
-
}
|
|
35
|
-
if cfg.tenant_id:
|
|
36
|
-
base["obtrace.tenant_id"] = cfg.tenant_id
|
|
37
|
-
if cfg.project_id:
|
|
38
|
-
base["obtrace.project_id"] = cfg.project_id
|
|
39
|
-
if cfg.app_id:
|
|
40
|
-
base["obtrace.app_id"] = cfg.app_id
|
|
41
|
-
if cfg.env:
|
|
42
|
-
base["obtrace.env"] = cfg.env
|
|
43
|
-
return _attrs(base)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def build_logs_payload(cfg: ObtraceConfig, level: str, body: str, ctx: Optional[SDKContext] = None) -> Dict[str, Any]:
|
|
47
|
-
context_attrs: Dict[str, Any] = {"obtrace.log.level": level}
|
|
48
|
-
if ctx:
|
|
49
|
-
if ctx.trace_id:
|
|
50
|
-
context_attrs["obtrace.trace_id"] = ctx.trace_id
|
|
51
|
-
if ctx.span_id:
|
|
52
|
-
context_attrs["obtrace.span_id"] = ctx.span_id
|
|
53
|
-
if ctx.session_id:
|
|
54
|
-
context_attrs["obtrace.session_id"] = ctx.session_id
|
|
55
|
-
if ctx.route_template:
|
|
56
|
-
context_attrs["obtrace.route_template"] = ctx.route_template
|
|
57
|
-
if ctx.endpoint:
|
|
58
|
-
context_attrs["obtrace.endpoint"] = ctx.endpoint
|
|
59
|
-
if ctx.method:
|
|
60
|
-
context_attrs["obtrace.method"] = ctx.method
|
|
61
|
-
if ctx.status_code is not None:
|
|
62
|
-
context_attrs["obtrace.status_code"] = ctx.status_code
|
|
63
|
-
for k, v in ctx.attrs.items():
|
|
64
|
-
context_attrs[f"obtrace.attr.{k}"] = v
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
"resourceLogs": [
|
|
68
|
-
{
|
|
69
|
-
"resource": {"attributes": _resource(cfg)},
|
|
70
|
-
"scopeLogs": [
|
|
71
|
-
{
|
|
72
|
-
"scope": {"name": "obtrace-sdk-python", "version": "1.0.0"},
|
|
73
|
-
"logRecords": [
|
|
74
|
-
{
|
|
75
|
-
"timeUnixNano": _now_unix_nano_str(),
|
|
76
|
-
"severityText": level.upper(),
|
|
77
|
-
"body": {"stringValue": body},
|
|
78
|
-
"attributes": _attrs(context_attrs),
|
|
79
|
-
}
|
|
80
|
-
],
|
|
81
|
-
}
|
|
82
|
-
],
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def build_metric_payload(
|
|
89
|
-
cfg: ObtraceConfig,
|
|
90
|
-
metric_name: str,
|
|
91
|
-
value: float,
|
|
92
|
-
unit: str = "1",
|
|
93
|
-
ctx: Optional[SDKContext] = None,
|
|
94
|
-
) -> Dict[str, Any]:
|
|
95
|
-
return {
|
|
96
|
-
"resourceMetrics": [
|
|
97
|
-
{
|
|
98
|
-
"resource": {"attributes": _resource(cfg)},
|
|
99
|
-
"scopeMetrics": [
|
|
100
|
-
{
|
|
101
|
-
"scope": {"name": "obtrace-sdk-python", "version": "1.0.0"},
|
|
102
|
-
"metrics": [
|
|
103
|
-
{
|
|
104
|
-
"name": metric_name,
|
|
105
|
-
"unit": unit,
|
|
106
|
-
"gauge": {
|
|
107
|
-
"dataPoints": [
|
|
108
|
-
{
|
|
109
|
-
"timeUnixNano": _now_unix_nano_str(),
|
|
110
|
-
"asDouble": float(value),
|
|
111
|
-
"attributes": _attrs(ctx.attrs if ctx else None),
|
|
112
|
-
}
|
|
113
|
-
]
|
|
114
|
-
},
|
|
115
|
-
}
|
|
116
|
-
],
|
|
117
|
-
}
|
|
118
|
-
],
|
|
119
|
-
}
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def build_span_payload(
|
|
125
|
-
cfg: ObtraceConfig,
|
|
126
|
-
name: str,
|
|
127
|
-
trace_id: str,
|
|
128
|
-
span_id: str,
|
|
129
|
-
start_unix_nano: str,
|
|
130
|
-
end_unix_nano: str,
|
|
131
|
-
status_code: Optional[int] = None,
|
|
132
|
-
status_message: str = "",
|
|
133
|
-
attrs: Optional[Dict[str, Any]] = None,
|
|
134
|
-
) -> Dict[str, Any]:
|
|
135
|
-
return {
|
|
136
|
-
"resourceSpans": [
|
|
137
|
-
{
|
|
138
|
-
"resource": {"attributes": _resource(cfg)},
|
|
139
|
-
"scopeSpans": [
|
|
140
|
-
{
|
|
141
|
-
"scope": {"name": "obtrace-sdk-python", "version": "1.0.0"},
|
|
142
|
-
"spans": [
|
|
143
|
-
{
|
|
144
|
-
"traceId": trace_id,
|
|
145
|
-
"spanId": span_id,
|
|
146
|
-
"name": name,
|
|
147
|
-
"kind": 3,
|
|
148
|
-
"startTimeUnixNano": start_unix_nano,
|
|
149
|
-
"endTimeUnixNano": end_unix_nano,
|
|
150
|
-
"attributes": _attrs(attrs),
|
|
151
|
-
"status": {
|
|
152
|
-
"code": 2 if (status_code is not None and status_code >= 400) else 1,
|
|
153
|
-
"message": status_message,
|
|
154
|
-
},
|
|
155
|
-
}
|
|
156
|
-
],
|
|
157
|
-
}
|
|
158
|
-
],
|
|
159
|
-
}
|
|
160
|
-
]
|
|
161
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from unittest.mock import patch, MagicMock
|
|
3
|
-
|
|
4
|
-
from obtrace_sdk.client import ObtraceClient
|
|
5
|
-
from obtrace_sdk.types import ObtraceConfig
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ClientTests(unittest.TestCase):
|
|
9
|
-
def test_enqueue_and_flush(self):
|
|
10
|
-
c = ObtraceClient(
|
|
11
|
-
ObtraceConfig(
|
|
12
|
-
api_key="devkey",
|
|
13
|
-
ingest_base_url="https://inject.obtrace.ai",
|
|
14
|
-
service_name="py-test",
|
|
15
|
-
)
|
|
16
|
-
)
|
|
17
|
-
c.log("info", "hello")
|
|
18
|
-
c.metric("m", 1)
|
|
19
|
-
c.span("s")
|
|
20
|
-
|
|
21
|
-
with patch("urllib.request.urlopen") as urlopen:
|
|
22
|
-
cm = MagicMock()
|
|
23
|
-
cm.__enter__.return_value.status = 202
|
|
24
|
-
cm.__exit__.return_value = False
|
|
25
|
-
urlopen.return_value = cm
|
|
26
|
-
c.flush()
|
|
27
|
-
|
|
28
|
-
self.assertEqual(urlopen.call_count, 3)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if __name__ == "__main__":
|
|
32
|
-
unittest.main()
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
from obtrace_sdk.context import create_traceparent, ensure_propagation_headers
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ContextTests(unittest.TestCase):
|
|
7
|
-
def test_create_traceparent(self):
|
|
8
|
-
tp = create_traceparent()
|
|
9
|
-
self.assertTrue(tp.startswith("00-"))
|
|
10
|
-
parts = tp.split("-")
|
|
11
|
-
self.assertEqual(len(parts[1]), 32)
|
|
12
|
-
self.assertEqual(len(parts[2]), 16)
|
|
13
|
-
|
|
14
|
-
def test_ensure_propagation_headers(self):
|
|
15
|
-
out = ensure_propagation_headers({}, session_id="s1")
|
|
16
|
-
self.assertIn("traceparent", {k.lower(): v for k, v in out.items()})
|
|
17
|
-
self.assertIn("x-obtrace-session-id", {k.lower(): v for k, v in out.items()})
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if __name__ == "__main__":
|
|
21
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{obtrace_sdk_python-1.0.0 → obtrace_sdk_python-2.0.0}/src/obtrace_sdk_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|