Skip to content

Commit c029d7c

Browse files
feat: Tracing. (#33)
* feat: Tracing. * feat: Add integration with opentelemetry. * fix: update tests * chore: add tests --------- Co-authored-by: Noah Hummel <[email protected]>
1 parent 3ff161f commit c029d7c

File tree

15 files changed

+743
-361
lines changed

15 files changed

+743
-361
lines changed

eventsourcingdb_client_python/event/event.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from typing import TypeVar
33

4+
from .tracing import TracingContext
45
from ..errors.validation_error import ValidationError
56
from .event_context import EventContext
67

@@ -18,7 +19,8 @@ def __init__(
1819
event_id: str,
1920
time: datetime,
2021
data_content_type: str,
21-
predecessor_hash: str
22+
predecessor_hash: str,
23+
tracing_context: TracingContext = None
2224
):
2325
super().__init__(
2426
source,
@@ -28,7 +30,8 @@ def __init__(
2830
event_id,
2931
time,
3032
data_content_type,
31-
predecessor_hash
33+
predecessor_hash,
34+
tracing_context
3235
)
3336
self.data = data
3437

@@ -50,7 +53,8 @@ def parse(unknown_object: dict) -> Self:
5053
event_context.event_id,
5154
event_context.time,
5255
event_context.data_content_type,
53-
event_context.predecessor_hash
56+
event_context.predecessor_hash,
57+
event_context.tracing_context
5458
)
5559

5660
def to_json(self):

eventsourcingdb_client_python/event/event_candidate.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from dataclasses import dataclass
22

3+
from .tracing import TracingContext
34
from .validate_subject import validate_subject
45
from .validate_type import validate_type
56

@@ -10,15 +11,21 @@ class EventCandidate:
1011
subject: str
1112
type: str
1213
data: dict
14+
tracing_context: TracingContext | None = None
1315

1416
def validate(self) -> None:
1517
validate_subject(self.subject)
1618
validate_type(self.type)
1719

1820
def to_json(self):
19-
return {
21+
json = {
2022
'data': self.data,
2123
'source': self.source,
2224
'subject': self.subject,
2325
'type': self.type
2426
}
27+
28+
if self.tracing_context is not None:
29+
json['tracingContext'] = self.tracing_context.to_json()
30+
31+
return json

eventsourcingdb_client_python/event/event_context.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import datetime
33
from typing import TypeVar
44

5+
from .tracing import TracingContext
56
from ..errors.internal_error import InternalError
67
from ..errors.validation_error import ValidationError
78
from .validate_subject import validate_subject
@@ -20,6 +21,7 @@ class EventContext:
2021
time: datetime
2122
data_content_type: str
2223
predecessor_hash: str
24+
tracing_context: TracingContext | None = None
2325

2426
@staticmethod
2527
def parse(unknown_object: dict) -> Self:
@@ -75,6 +77,11 @@ def parse(unknown_object: dict) -> Self:
7577
raise ValidationError(
7678
f'Failed to parse predecessor_hash \'{predecessor_hash}\' to string.')
7779

80+
raw_tracing_context = unknown_object.get("tracingContext")
81+
tracing_context = TracingContext.parse(raw_tracing_context) \
82+
if raw_tracing_context is not None \
83+
else None
84+
7885
return EventContext(
7986
source=source,
8087
subject=subject,
@@ -83,7 +90,8 @@ def parse(unknown_object: dict) -> Self:
8390
event_id=event_id,
8491
time=time,
8592
data_content_type=data_content_type,
86-
predecessor_hash=predecessor_hash
93+
predecessor_hash=predecessor_hash,
94+
tracing_context=tracing_context
8795
)
8896

8997
def to_json(self):

eventsourcingdb_client_python/event/source.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
22

33
from eventsourcingdb_client_python.event.event_candidate import EventCandidate
4+
from eventsourcingdb_client_python.event.tracing import TracingContext
45

56

67
@dataclass
@@ -11,6 +12,7 @@ def new_event(
1112
self,
1213
subject: str,
1314
event_type: str,
14-
data: dict
15+
data: dict,
16+
tracing_context: TracingContext = None
1517
) -> EventCandidate:
16-
return EventCandidate(self.source, subject, event_type, data)
18+
return EventCandidate(self.source, subject, event_type, data, tracing_context)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from dataclasses import dataclass
2+
from typing import TypeVar
3+
4+
from opentelemetry.trace import TraceState, TraceFlags, SpanContext, format_trace_id, format_span_id
5+
6+
from ..errors.validation_error import ValidationError
7+
8+
Self = TypeVar("Self", bound="TracingContext")
9+
10+
11+
@dataclass
12+
class TracingContext:
13+
trace_id: str
14+
span_id: str
15+
trace_flags: TraceFlags = TraceFlags.DEFAULT
16+
trace_state: TraceState = TraceState()
17+
18+
@staticmethod
19+
def from_span_context(span_context: SpanContext) -> Self:
20+
return TracingContext(
21+
trace_id=format_trace_id(span_context.trace_id),
22+
span_id=format_span_id(span_context.span_id),
23+
trace_flags=span_context.trace_flags,
24+
trace_state=span_context.trace_state
25+
)
26+
27+
def trace_flags_to_string(self) -> str:
28+
return format(self.trace_flags, "02x")
29+
30+
def trace_parent(self) -> str:
31+
return f'00-{self.trace_id}-{self.span_id}-{self.trace_flags_to_string()}'
32+
33+
def to_opentelemetry_context_carrier(self):
34+
return {
35+
'traceparent': self.trace_parent(),
36+
'tracestate': self.trace_state.to_header()
37+
}
38+
39+
@staticmethod
40+
def parse(unknown_object: dict) -> Self:
41+
trace_id = unknown_object.get("traceId")
42+
if not isinstance(trace_id, str):
43+
raise ValidationError(
44+
f'Failed to parse trace id \'{trace_id}\' to string.')
45+
46+
span_id = unknown_object.get("spanId")
47+
if not isinstance(span_id, str):
48+
raise ValidationError(
49+
f'Failed to parse span id \'{span_id}\' to string.')
50+
51+
raw_trace_flags = unknown_object.get("traceFlags")
52+
if not isinstance(raw_trace_flags, str):
53+
raise ValidationError(
54+
f'Failed to parse trace flags \'{raw_trace_flags}\' to string.')
55+
if raw_trace_flags == format(TraceFlags.DEFAULT, "02x"):
56+
trace_flags = TraceFlags.DEFAULT
57+
elif raw_trace_flags == format(TraceFlags.SAMPLED, "02x"):
58+
trace_flags = TraceFlags.SAMPLED
59+
else:
60+
raise ValidationError(
61+
'Trace flags must be either None (0) or Sampled (1).')
62+
63+
raw_trace_state = unknown_object.get("traceState")
64+
if not isinstance(raw_trace_state, str):
65+
raise ValidationError(
66+
f'Failed to parse trace state \'{raw_trace_state}\' to string.')
67+
trace_state = TraceState.from_header([raw_trace_state])
68+
69+
return TracingContext(
70+
trace_id=trace_id,
71+
span_id=span_id,
72+
trace_flags=trace_flags,
73+
trace_state=trace_state
74+
)
75+
76+
def to_json(self):
77+
return {
78+
'traceId': self.trace_id,
79+
'spanId': self.span_id,
80+
'traceFlags': self.trace_flags_to_string(),
81+
'traceState': self.trace_state.to_header()
82+
}

0 commit comments

Comments
 (0)