spanwire 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.
- spanwire/__init__.py +145 -0
- spanwire/_version.py +1 -0
- spanwire/breadcrumbs.py +88 -0
- spanwire/context.py +59 -0
- spanwire/core.py +463 -0
- spanwire/exporters.py +188 -0
- spanwire/py.typed +0 -0
- spanwire/sampler.py +108 -0
- spanwire/w3c.py +160 -0
- spanwire-0.1.0.dist-info/METADATA +226 -0
- spanwire-0.1.0.dist-info/RECORD +14 -0
- spanwire-0.1.0.dist-info/WHEEL +5 -0
- spanwire-0.1.0.dist-info/licenses/LICENSE +21 -0
- spanwire-0.1.0.dist-info/top_level.txt +1 -0
spanwire/__init__.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""spanwire: Zero-dependency micro-tracing, distributed context propagation & breadcrumb telemetry engine for Python."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
from spanwire._version import __version__
|
|
8
|
+
from spanwire.breadcrumbs import Breadcrumb, BreadcrumbBuffer
|
|
9
|
+
from spanwire.context import (
|
|
10
|
+
clear_baggage,
|
|
11
|
+
get_baggage,
|
|
12
|
+
get_baggage_item,
|
|
13
|
+
get_current_span,
|
|
14
|
+
set_baggage,
|
|
15
|
+
set_baggage_item,
|
|
16
|
+
)
|
|
17
|
+
from spanwire.core import (
|
|
18
|
+
Span,
|
|
19
|
+
SpanContext,
|
|
20
|
+
SpanEvent,
|
|
21
|
+
SpanStatus,
|
|
22
|
+
Tracer,
|
|
23
|
+
generate_span_id,
|
|
24
|
+
generate_trace_id,
|
|
25
|
+
)
|
|
26
|
+
from spanwire.exporters import (
|
|
27
|
+
CallbackExporter,
|
|
28
|
+
ConsoleExporter,
|
|
29
|
+
InMemoryExporter,
|
|
30
|
+
JsonLinesExporter,
|
|
31
|
+
SpanExporter,
|
|
32
|
+
)
|
|
33
|
+
from spanwire.sampler import (
|
|
34
|
+
AlwaysOffSampler,
|
|
35
|
+
AlwaysOnSampler,
|
|
36
|
+
ParentBasedSampler,
|
|
37
|
+
RatioSampler,
|
|
38
|
+
Sampler,
|
|
39
|
+
)
|
|
40
|
+
from spanwire.w3c import (
|
|
41
|
+
extract_w3c_headers,
|
|
42
|
+
format_baggage,
|
|
43
|
+
format_traceparent,
|
|
44
|
+
inject_w3c_headers,
|
|
45
|
+
is_valid_span_id,
|
|
46
|
+
is_valid_trace_id,
|
|
47
|
+
parse_baggage,
|
|
48
|
+
parse_traceparent,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_DEFAULT_TRACER: Optional[Tracer] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_tracer(service_name: str = "default-service") -> Tracer:
|
|
55
|
+
"""Get or initialize the global default Tracer."""
|
|
56
|
+
global _DEFAULT_TRACER
|
|
57
|
+
if _DEFAULT_TRACER is None:
|
|
58
|
+
_DEFAULT_TRACER = Tracer(service_name=service_name)
|
|
59
|
+
return _DEFAULT_TRACER
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def set_tracer(tracer: Tracer) -> None:
|
|
63
|
+
"""Set the global default Tracer."""
|
|
64
|
+
global _DEFAULT_TRACER
|
|
65
|
+
_DEFAULT_TRACER = tracer
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def span(
|
|
69
|
+
name: str,
|
|
70
|
+
attributes: Optional[Mapping[str, Any]] = None,
|
|
71
|
+
parent_context: Optional[SpanContext] = None,
|
|
72
|
+
) -> Span:
|
|
73
|
+
"""Create a span on the default global tracer."""
|
|
74
|
+
return get_tracer().span(name, attributes=attributes, parent_context=parent_context)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def trace(
|
|
78
|
+
name: Optional[str] = None,
|
|
79
|
+
attributes: Optional[Mapping[str, Any]] = None,
|
|
80
|
+
) -> Callable[..., Any]:
|
|
81
|
+
"""Decorator to trace a function using the default global tracer."""
|
|
82
|
+
return get_tracer().trace(name=name, attributes=attributes)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def add_breadcrumb(
|
|
86
|
+
message: str,
|
|
87
|
+
level: str = "INFO",
|
|
88
|
+
category: str = "default",
|
|
89
|
+
data: Optional[Dict[str, Any]] = None,
|
|
90
|
+
**kwargs: Any,
|
|
91
|
+
) -> Breadcrumb:
|
|
92
|
+
"""Add a breadcrumb to the default global tracer."""
|
|
93
|
+
return get_tracer().add_breadcrumb(
|
|
94
|
+
message=message, level=level, category=category, data=data, **kwargs
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = [
|
|
99
|
+
"__version__",
|
|
100
|
+
# Core
|
|
101
|
+
"Tracer",
|
|
102
|
+
"Span",
|
|
103
|
+
"SpanContext",
|
|
104
|
+
"SpanEvent",
|
|
105
|
+
"SpanStatus",
|
|
106
|
+
"generate_trace_id",
|
|
107
|
+
"generate_span_id",
|
|
108
|
+
# Global conveniences
|
|
109
|
+
"get_tracer",
|
|
110
|
+
"set_tracer",
|
|
111
|
+
"span",
|
|
112
|
+
"trace",
|
|
113
|
+
"add_breadcrumb",
|
|
114
|
+
# Context
|
|
115
|
+
"get_current_span",
|
|
116
|
+
"get_baggage",
|
|
117
|
+
"set_baggage",
|
|
118
|
+
"set_baggage_item",
|
|
119
|
+
"get_baggage_item",
|
|
120
|
+
"clear_baggage",
|
|
121
|
+
# Breadcrumbs
|
|
122
|
+
"Breadcrumb",
|
|
123
|
+
"BreadcrumbBuffer",
|
|
124
|
+
# Samplers
|
|
125
|
+
"Sampler",
|
|
126
|
+
"AlwaysOnSampler",
|
|
127
|
+
"AlwaysOffSampler",
|
|
128
|
+
"RatioSampler",
|
|
129
|
+
"ParentBasedSampler",
|
|
130
|
+
# Exporters
|
|
131
|
+
"SpanExporter",
|
|
132
|
+
"InMemoryExporter",
|
|
133
|
+
"JsonLinesExporter",
|
|
134
|
+
"ConsoleExporter",
|
|
135
|
+
"CallbackExporter",
|
|
136
|
+
# W3C
|
|
137
|
+
"format_traceparent",
|
|
138
|
+
"parse_traceparent",
|
|
139
|
+
"format_baggage",
|
|
140
|
+
"parse_baggage",
|
|
141
|
+
"inject_w3c_headers",
|
|
142
|
+
"extract_w3c_headers",
|
|
143
|
+
"is_valid_trace_id",
|
|
144
|
+
"is_valid_span_id",
|
|
145
|
+
]
|
spanwire/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
spanwire/breadcrumbs.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Breadcrumb trail telemetry buffer for debugging contextual application state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import collections
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Breadcrumb:
|
|
14
|
+
"""An individual recorded breadcrumb milestone in the telemetry trail."""
|
|
15
|
+
|
|
16
|
+
message: str
|
|
17
|
+
level: str = "INFO"
|
|
18
|
+
category: str = "default"
|
|
19
|
+
timestamp: float = field(default_factory=time.time)
|
|
20
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
23
|
+
"""Convert breadcrumb to dictionary representation."""
|
|
24
|
+
return {
|
|
25
|
+
"message": self.message,
|
|
26
|
+
"level": self.level.upper(),
|
|
27
|
+
"category": self.category,
|
|
28
|
+
"timestamp": self.timestamp,
|
|
29
|
+
"data": dict(self.data),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BreadcrumbBuffer:
|
|
34
|
+
"""Thread-safe bounded ring buffer holding recent breadcrumbs."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, capacity: int = 50) -> None:
|
|
37
|
+
if capacity <= 0:
|
|
38
|
+
raise ValueError(f"Capacity must be positive, got {capacity}")
|
|
39
|
+
self.capacity = capacity
|
|
40
|
+
self._deque: collections.deque[Breadcrumb] = collections.deque(maxlen=capacity)
|
|
41
|
+
self._lock = threading.Lock()
|
|
42
|
+
|
|
43
|
+
def add(self, crumb: Breadcrumb) -> None:
|
|
44
|
+
"""Add an existing breadcrumb into the buffer."""
|
|
45
|
+
with self._lock:
|
|
46
|
+
self._deque.append(crumb)
|
|
47
|
+
|
|
48
|
+
def record(
|
|
49
|
+
self,
|
|
50
|
+
message: str,
|
|
51
|
+
level: str = "INFO",
|
|
52
|
+
category: str = "default",
|
|
53
|
+
data: Optional[Dict[str, Any]] = None,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
) -> Breadcrumb:
|
|
56
|
+
"""Create and record a breadcrumb in one step."""
|
|
57
|
+
merged_data: Dict[str, Any] = {}
|
|
58
|
+
if data:
|
|
59
|
+
merged_data.update(data)
|
|
60
|
+
if kwargs:
|
|
61
|
+
merged_data.update(kwargs)
|
|
62
|
+
|
|
63
|
+
crumb = Breadcrumb(
|
|
64
|
+
message=message,
|
|
65
|
+
level=level.upper(),
|
|
66
|
+
category=category,
|
|
67
|
+
timestamp=time.time(),
|
|
68
|
+
data=merged_data,
|
|
69
|
+
)
|
|
70
|
+
self.add(crumb)
|
|
71
|
+
return crumb
|
|
72
|
+
|
|
73
|
+
def get_crumbs(self) -> List[Breadcrumb]:
|
|
74
|
+
"""Return a snapshot copy of current breadcrumbs in chronological order."""
|
|
75
|
+
with self._lock:
|
|
76
|
+
return list(self._deque)
|
|
77
|
+
|
|
78
|
+
def clear(self) -> None:
|
|
79
|
+
"""Clear all stored breadcrumbs."""
|
|
80
|
+
with self._lock:
|
|
81
|
+
self._deque.clear()
|
|
82
|
+
|
|
83
|
+
def __len__(self) -> int:
|
|
84
|
+
with self._lock:
|
|
85
|
+
return len(self._deque)
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
return f"BreadcrumbBuffer(count={len(self)}, capacity={self.capacity})"
|
spanwire/context.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Context storage and propagation using Python contextvars for sync and async code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextvars
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from spanwire.core import Span
|
|
10
|
+
|
|
11
|
+
_CURRENT_SPAN: contextvars.ContextVar[Optional[Span]] = contextvars.ContextVar(
|
|
12
|
+
"spanwire_current_span", default=None
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
_CURRENT_BAGGAGE: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
|
|
16
|
+
"spanwire_current_baggage", default={}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_current_span() -> Optional[Span]:
|
|
21
|
+
"""Retrieve the currently active Span in the current execution context."""
|
|
22
|
+
return _CURRENT_SPAN.get()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_current_span(span: Optional[Span]) -> contextvars.Token[Optional[Span]]:
|
|
26
|
+
"""Set the active Span and return the restoration token."""
|
|
27
|
+
return _CURRENT_SPAN.set(span)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reset_current_span(token: contextvars.Token[Optional[Span]]) -> None:
|
|
31
|
+
"""Reset the active Span to its state prior to set_current_span."""
|
|
32
|
+
_CURRENT_SPAN.reset(token)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_baggage() -> Dict[str, str]:
|
|
36
|
+
"""Get a copy of the current context's baggage key-values."""
|
|
37
|
+
return dict(_CURRENT_BAGGAGE.get())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def set_baggage(baggage: Dict[str, str]) -> contextvars.Token[Dict[str, str]]:
|
|
41
|
+
"""Set the entire baggage dictionary in context."""
|
|
42
|
+
return _CURRENT_BAGGAGE.set(dict(baggage))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def set_baggage_item(key: str, value: str) -> None:
|
|
46
|
+
"""Set or update an individual baggage item in the current context."""
|
|
47
|
+
current = dict(_CURRENT_BAGGAGE.get())
|
|
48
|
+
current[str(key).strip()] = str(value).strip()
|
|
49
|
+
_CURRENT_BAGGAGE.set(current)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_baggage_item(key: str, default: Optional[str] = None) -> Optional[str]:
|
|
53
|
+
"""Get an individual baggage item value from context."""
|
|
54
|
+
return _CURRENT_BAGGAGE.get().get(str(key).strip(), default)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def clear_baggage() -> None:
|
|
58
|
+
"""Clear all baggage in the current context."""
|
|
59
|
+
_CURRENT_BAGGAGE.set({})
|