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 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"
@@ -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({})