Skip to content

Commit b965b0f

Browse files
CopilotnikhilNava
andcommitted
Move parse_parent_id_to_context to utils.py
Refactored W3C Trace Context parsing: - Added W3C Trace Context validation constants - Added validate_w3c_trace_context_version() helper - Added validate_trace_id() helper with hex validation - Added validate_span_id() helper with hex validation - Moved parse_parent_id_to_context() from opentelemetry_scope.py Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
1 parent f902aad commit b965b0f

File tree

2 files changed

+126
-69
lines changed

2 files changed

+126
-69
lines changed

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/opentelemetry_scope.py

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@
1111

1212
from opentelemetry import baggage, context, trace
1313
from opentelemetry.trace import (
14-
NonRecordingSpan,
1514
Span,
16-
SpanContext,
1715
SpanKind,
1816
Status,
1917
StatusCode,
20-
TraceFlags,
2118
Tracer,
2219
set_span_in_context,
2320
)
@@ -42,6 +39,7 @@
4239
SOURCE_NAME,
4340
TENANT_ID_KEY,
4441
)
42+
from .utils import parse_parent_id_to_context
4543

4644
if TYPE_CHECKING:
4745
from .agent_details import AgentDetails
@@ -51,70 +49,6 @@
5149
logger = logging.getLogger(__name__)
5250

5351

54-
def _parse_parent_id_to_context(parent_id: str | None) -> context.Context | None:
55-
"""Parse a W3C trace context parent ID and return a context with the parent span.
56-
57-
The parent_id format is expected to be W3C Trace Context format:
58-
"00-{trace_id}-{span_id}-{trace_flags}"
59-
Example: "00-1234567890abcdef1234567890abcdef-abcdefabcdef1234-01"
60-
61-
Args:
62-
parent_id: The W3C Trace Context format parent ID string
63-
64-
Returns:
65-
A context containing the parent span, or None if parent_id is invalid
66-
"""
67-
if not parent_id:
68-
return None
69-
70-
try:
71-
# W3C Trace Context format: "00-{trace_id}-{span_id}-{trace_flags}"
72-
parts = parent_id.split("-")
73-
if len(parts) != 4:
74-
logger.warning(f"Invalid parent_id format (expected 4 parts): {parent_id}")
75-
return None
76-
77-
version, trace_id_hex, span_id_hex, trace_flags_hex = parts
78-
79-
# Validate W3C Trace Context version
80-
if version != "00":
81-
logger.warning(f"Unsupported W3C Trace Context version: {version}")
82-
return None
83-
84-
# Validate trace_id length (must be 32 hex chars)
85-
if len(trace_id_hex) != 32:
86-
logger.warning(f"Invalid trace_id length (expected 32 chars): {len(trace_id_hex)}")
87-
return None
88-
89-
# Validate span_id length (must be 16 hex chars)
90-
if len(span_id_hex) != 16:
91-
logger.warning(f"Invalid span_id length (expected 16 chars): {len(span_id_hex)}")
92-
return None
93-
94-
# Parse the hex values
95-
trace_id = int(trace_id_hex, 16)
96-
span_id = int(span_id_hex, 16)
97-
trace_flags = TraceFlags(int(trace_flags_hex, 16))
98-
99-
# Create a SpanContext from the parsed values
100-
parent_span_context = SpanContext(
101-
trace_id=trace_id,
102-
span_id=span_id,
103-
is_remote=True,
104-
trace_flags=trace_flags,
105-
)
106-
107-
# Create a NonRecordingSpan with the parent context
108-
parent_span = NonRecordingSpan(parent_span_context)
109-
110-
# Create a context with the parent span
111-
return set_span_in_context(parent_span)
112-
113-
except (ValueError, IndexError) as e:
114-
logger.warning(f"Failed to parse parent_id '{parent_id}': {e}")
115-
return None
116-
117-
11852
class OpenTelemetryScope:
11953
"""Base class for OpenTelemetry tracing scopes in the SDK."""
12054

@@ -182,7 +116,7 @@ def __init__(
182116
# Get context for parent relationship
183117
# If parent_id is provided, parse it and use it as the parent context
184118
# Otherwise, use the current context
185-
parent_context = _parse_parent_id_to_context(parent_id)
119+
parent_context = parse_parent_id_to_context(parent_id)
186120
span_context = parent_context if parent_context else context.get_current()
187121

188122
self._span = tracer.start_span(activity_name, kind=activity_kind, context=span_context)

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/utils.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
from threading import RLock
1414
from typing import Any, Generic, TypeVar, cast
1515

16+
from opentelemetry import context
1617
from opentelemetry.semconv.attributes.exception_attributes import (
1718
EXCEPTION_MESSAGE,
1819
EXCEPTION_STACKTRACE,
1920
)
20-
from opentelemetry.trace import Span
21+
from opentelemetry.trace import NonRecordingSpan, Span, SpanContext, TraceFlags, set_span_in_context
2122
from opentelemetry.util.types import AttributeValue
2223
from wrapt import ObjectProxy
2324

@@ -27,6 +28,128 @@
2728
logger.addHandler(logging.NullHandler())
2829

2930

31+
# W3C Trace Context constants
32+
W3C_TRACE_CONTEXT_VERSION = "00"
33+
W3C_TRACE_ID_LENGTH = 32 # 32 hex chars = 128 bits
34+
W3C_SPAN_ID_LENGTH = 16 # 16 hex chars = 64 bits
35+
36+
37+
def validate_w3c_trace_context_version(version: str) -> bool:
38+
"""Validate W3C Trace Context version.
39+
40+
Args:
41+
version: The version string to validate
42+
43+
Returns:
44+
True if valid, False otherwise
45+
"""
46+
return version == W3C_TRACE_CONTEXT_VERSION
47+
48+
49+
def _is_valid_hex(hex_string: str) -> bool:
50+
"""Check if a string contains only valid hexadecimal characters.
51+
52+
Args:
53+
hex_string: The string to validate
54+
55+
Returns:
56+
True if all characters are valid hexadecimal (0-9, a-f, A-F), False otherwise
57+
"""
58+
return all(c in "0123456789abcdefABCDEF" for c in hex_string)
59+
60+
61+
def validate_trace_id(trace_id_hex: str) -> bool:
62+
"""Validate W3C Trace Context trace_id format.
63+
64+
Args:
65+
trace_id_hex: The trace_id hex string to validate (should be 32 hex chars)
66+
67+
Returns:
68+
True if valid (32 hex chars), False otherwise
69+
"""
70+
return len(trace_id_hex) == W3C_TRACE_ID_LENGTH and _is_valid_hex(trace_id_hex)
71+
72+
73+
def validate_span_id(span_id_hex: str) -> bool:
74+
"""Validate W3C Trace Context span_id format.
75+
76+
Args:
77+
span_id_hex: The span_id hex string to validate (should be 16 hex chars)
78+
79+
Returns:
80+
True if valid (16 hex chars), False otherwise
81+
"""
82+
return len(span_id_hex) == W3C_SPAN_ID_LENGTH and _is_valid_hex(span_id_hex)
83+
84+
85+
def parse_parent_id_to_context(parent_id: str | None) -> context.Context | None:
86+
"""Parse a W3C trace context parent ID and return a context with the parent span.
87+
88+
The parent_id format is expected to be W3C Trace Context format:
89+
"00-{trace_id}-{span_id}-{trace_flags}"
90+
Example: "00-1234567890abcdef1234567890abcdef-abcdefabcdef1234-01"
91+
92+
Args:
93+
parent_id: The W3C Trace Context format parent ID string
94+
95+
Returns:
96+
A context containing the parent span, or None if parent_id is invalid
97+
"""
98+
if not parent_id:
99+
return None
100+
101+
try:
102+
# W3C Trace Context format: "00-{trace_id}-{span_id}-{trace_flags}"
103+
parts = parent_id.split("-")
104+
if len(parts) != 4:
105+
logger.warning(f"Invalid parent_id format (expected 4 parts): {parent_id}")
106+
return None
107+
108+
version, trace_id_hex, span_id_hex, trace_flags_hex = parts
109+
110+
# Validate W3C Trace Context version
111+
if not validate_w3c_trace_context_version(version):
112+
logger.warning(f"Unsupported W3C Trace Context version: {version}")
113+
return None
114+
115+
# Validate trace_id (must be 32 hex chars)
116+
if not validate_trace_id(trace_id_hex):
117+
logger.warning(
118+
f"Invalid trace_id (expected {W3C_TRACE_ID_LENGTH} hex chars): '{trace_id_hex}'"
119+
)
120+
return None
121+
122+
# Validate span_id (must be 16 hex chars)
123+
if not validate_span_id(span_id_hex):
124+
logger.warning(
125+
f"Invalid span_id (expected {W3C_SPAN_ID_LENGTH} hex chars): '{span_id_hex}'"
126+
)
127+
return None
128+
129+
# Parse the hex values
130+
trace_id = int(trace_id_hex, 16)
131+
span_id = int(span_id_hex, 16)
132+
trace_flags = TraceFlags(int(trace_flags_hex, 16))
133+
134+
# Create a SpanContext from the parsed values
135+
parent_span_context = SpanContext(
136+
trace_id=trace_id,
137+
span_id=span_id,
138+
is_remote=True,
139+
trace_flags=trace_flags,
140+
)
141+
142+
# Create a NonRecordingSpan with the parent context
143+
parent_span = NonRecordingSpan(parent_span_context)
144+
145+
# Create a context with the parent span
146+
return set_span_in_context(parent_span)
147+
148+
except (ValueError, IndexError) as e:
149+
logger.warning(f"Failed to parse parent_id '{parent_id}': {e}")
150+
return None
151+
152+
30153
def safe_json_dumps(obj: Any, **kwargs: Any) -> str:
31154
return json.dumps(obj, default=str, ensure_ascii=False, **kwargs)
32155

0 commit comments

Comments
 (0)