Skip to content

Commit 2a3dc66

Browse files
committed
Enable telemetry collection in container flow with python az monitor hopper
1 parent e1f69b9 commit 2a3dc66

File tree

7 files changed

+375
-14
lines changed

7 files changed

+375
-14
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
asgiref==3.8.1
2+
asttokens==3.0.0
3+
attrs==25.3.0
4+
azure-core==1.34.0
5+
azure-core-tracing-opentelemetry==1.0.0b12
6+
azure-identity==1.23.0
7+
azure-monitor-opentelemetry==1.6.9
8+
azure-monitor-opentelemetry-exporter==1.0.0b36
9+
backcall==0.2.0
10+
beautifulsoup4==4.13.4
11+
bleach==6.2.0
12+
certifi==2025.4.26
13+
cffi==1.17.1
14+
charset-normalizer==3.4.2
15+
cryptography==44.0.3
16+
decorator==5.2.1
17+
defusedxml==0.7.1
18+
Deprecated==1.2.18
19+
docopt==0.6.2
20+
executing==2.2.0
21+
fastjsonschema==2.21.1
22+
fixedint==0.1.6
23+
googleapis-common-protos==1.70.0
24+
grpcio==1.71.0
25+
idna==3.10
26+
importlib_metadata==8.6.1
27+
ipython==8.12.3
28+
isodate==0.7.2
29+
jedi==0.19.2
30+
Jinja2==3.1.6
31+
jsonschema==4.24.0
32+
jsonschema-specifications==2025.4.1
33+
jupyter_client==8.6.3
34+
jupyter_core==5.8.1
35+
jupyterlab_pygments==0.3.0
36+
MarkupSafe==3.0.2
37+
matplotlib-inline==0.1.7
38+
mistune==3.1.3
39+
msal==1.32.3
40+
msal-extensions==1.3.1
41+
msrest==0.7.1
42+
nbclient==0.10.2
43+
nbconvert==7.16.6
44+
nbformat==5.10.4
45+
oauthlib==3.2.2
46+
opentelemetry-api==1.31.1
47+
opentelemetry-exporter-otlp==1.31.1
48+
opentelemetry-exporter-otlp-proto-common==1.31.1
49+
opentelemetry-exporter-otlp-proto-grpc==1.31.1
50+
opentelemetry-exporter-otlp-proto-http==1.31.1
51+
opentelemetry-instrumentation==0.52b1
52+
opentelemetry-instrumentation-asgi==0.52b1
53+
opentelemetry-instrumentation-dbapi==0.52b1
54+
opentelemetry-instrumentation-django==0.52b1
55+
opentelemetry-instrumentation-fastapi==0.52b1
56+
opentelemetry-instrumentation-flask==0.52b1
57+
opentelemetry-instrumentation-grpc==0.52b1
58+
opentelemetry-instrumentation-psycopg2==0.52b1
59+
opentelemetry-instrumentation-requests==0.52b1
60+
opentelemetry-instrumentation-urllib==0.52b1
61+
opentelemetry-instrumentation-urllib3==0.52b1
62+
opentelemetry-instrumentation-wsgi==0.52b1
63+
opentelemetry-proto==1.31.1
64+
opentelemetry-resource-detector-azure==0.1.5
65+
opentelemetry-sdk==1.31.1
66+
opentelemetry-semantic-conventions==0.52b1
67+
opentelemetry-util-http==0.52b1
68+
packaging==25.0
69+
pandocfilters==1.5.1
70+
parso==0.8.4
71+
pexpect==4.9.0
72+
pickleshare==0.7.5
73+
pipreqs==0.5.0
74+
platformdirs==4.3.8
75+
prompt_toolkit==3.0.51
76+
protobuf==5.29.4
77+
psutil==6.1.1
78+
ptyprocess==0.7.0
79+
pure_eval==0.2.3
80+
pycparser==2.22
81+
Pygments==2.19.1
82+
PyJWT==2.10.1
83+
python-dateutil==2.9.0.post0
84+
pyzmq==26.4.0
85+
referencing==0.36.2
86+
requests==2.32.3
87+
requests-oauthlib==2.0.0
88+
rpds-py==0.25.1
89+
six==1.17.0
90+
soupsieve==2.7
91+
stack-data==0.6.3
92+
tinycss2==1.4.0
93+
tornado==6.5.1
94+
traitlets==5.14.3
95+
typing_extensions==4.13.2
96+
urllib3==2.4.0
97+
wcwidth==0.2.13
98+
webencodings==0.5.1
99+
wrapt==1.17.2
100+
yarg==0.1.9
101+
zipp==3.21.0
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import os
2+
import logging
3+
import grpc
4+
import signal
5+
from concurrent import futures
6+
from typing import Dict, Any, List, Optional
7+
8+
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
9+
from opentelemetry import trace
10+
from opentelemetry.trace.status import Status, StatusCode
11+
from opentelemetry.sdk.resources import Resource
12+
from opentelemetry.sdk.trace import TracerProvider
13+
from opentelemetry.trace import SpanContext, TraceFlags, TraceState
14+
from opentelemetry.proto.collector.trace.v1 import (
15+
trace_service_pb2,
16+
trace_service_pb2_grpc,
17+
)
18+
from opentelemetry.proto.trace.v1.trace_pb2 import Span as ProtoSpan
19+
from opentelemetry.proto.common.v1.common_pb2 import KeyValue
20+
21+
DEFAULT_GRPC_PORT = 4317
22+
SHUTDOWN_GRACE_PERIOD_SEC = 5
23+
24+
AZURE_CONN_STR = "InstrumentationKey=c0b360fa-422d-40e5-b8a9-d642578f9fce;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;ApplicationId=087d527c-b60e-4346-a679-f6abf367d0f0"
25+
26+
27+
logging.basicConfig(level=logging.INFO)
28+
logger = logging.getLogger("image-customizer-telemetry")
29+
30+
31+
class SpanData:
32+
"""SpanData class for Azure Monitor export."""
33+
34+
def __init__(
35+
self, proto_span: ProtoSpan, resource_attrs: Dict[str, Any], inst_scope: Any
36+
) -> None:
37+
try:
38+
self.name = proto_span.name
39+
self.start_time = proto_span.start_time_unix_nano
40+
self.end_time = proto_span.end_time_unix_nano
41+
self.kind = proto_span.kind
42+
43+
self.attributes = self._set_attributes(
44+
proto_span.attributes, resource_attrs
45+
)
46+
self.status = self._extract_status(proto_span)
47+
self.events = proto_span.events
48+
self.links = proto_span.links
49+
self.context = self._create_span_context(
50+
proto_span.trace_id, proto_span.span_id
51+
)
52+
self.parent = self._create_span_context(
53+
proto_span.trace_id, proto_span.parent_span_id
54+
)
55+
self.resource = Resource.create(resource_attrs)
56+
self.instrumentation_scope = inst_scope
57+
58+
except Exception as e:
59+
logger.error(f"Failed to initialize SpanData: {e}")
60+
raise
61+
62+
def _set_attributes(
63+
self, proto_attributes: List[KeyValue], resource_attrs: Dict[str, Any]
64+
) -> Dict[str, Any]:
65+
attributes = dict(resource_attrs)
66+
67+
span_attrs = extract_attributes_from_proto(proto_attributes)
68+
attributes.update(span_attrs)
69+
70+
return attributes
71+
72+
def _extract_status(self, proto_span: ProtoSpan) -> Status:
73+
if proto_span.HasField("status"):
74+
return Status(
75+
status_code=StatusCode(proto_span.status.code),
76+
description=proto_span.status.message or None,
77+
)
78+
return Status(StatusCode.UNSET)
79+
80+
def _create_span_context(self, trace_id, span_id) -> SpanContext:
81+
return SpanContext(
82+
trace_id=int.from_bytes(trace_id, "big"),
83+
span_id=int.from_bytes(span_id, "big"),
84+
is_remote=True,
85+
trace_flags=TraceFlags(0),
86+
trace_state=TraceState(),
87+
)
88+
89+
90+
class TraceServiceHandler(trace_service_pb2_grpc.TraceServiceServicer):
91+
"""OTLP trace service handler that forwards traces to Azure Monitor."""
92+
93+
def __init__(self) -> None:
94+
"""Initialize the trace service handler."""
95+
self.exporter = self._initialize_telemetry()
96+
97+
def _initialize_telemetry(self) -> AzureMonitorTraceExporter:
98+
"""Initialize OpenTelemetry and Azure Monitor exporter."""
99+
provider = TracerProvider(resource=Resource.create({}))
100+
trace.set_tracer_provider(provider)
101+
102+
return AzureMonitorTraceExporter(connection_string=AZURE_CONN_STR)
103+
104+
def Export(self, request, context) -> trace_service_pb2.ExportTraceServiceResponse:
105+
"""Export traces to Azure Monitor."""
106+
try:
107+
spans = self._process_trace_request(request)
108+
109+
if spans:
110+
result = self.exporter.export(spans)
111+
logger.info(
112+
"Successfully exported %d spans to Azure Monitor (result: %s)",
113+
len(spans),
114+
result,
115+
)
116+
return trace_service_pb2.ExportTraceServiceResponse()
117+
118+
except Exception as e:
119+
logger.error("Error processing spans: %s", e, exc_info=True)
120+
context.set_code(grpc.StatusCode.INTERNAL)
121+
context.set_details(f"Failed to process spans: {str(e)}")
122+
return trace_service_pb2.ExportTraceServiceResponse()
123+
124+
def _process_trace_request(self, request) -> List[SpanData]:
125+
"""Process trace request and convert to SpanData objects."""
126+
spans = []
127+
128+
for rs in request.resource_spans:
129+
resource_attrs = extract_attributes_from_proto(rs.resource.attributes)
130+
131+
for ss in rs.scope_spans:
132+
for proto_span in ss.spans:
133+
try:
134+
span_data = SpanData(proto_span, resource_attrs, ss.scope)
135+
spans.append(span_data)
136+
except Exception as e:
137+
logger.warning(f"Failed to process span {proto_span.name}: {e}")
138+
139+
return spans
140+
141+
142+
# Utility functions for protobuf attribute extraction
143+
def extract_attribute_value(value_proto: Any) -> Optional[Any]:
144+
"""Extract value from protobuf AnyValue."""
145+
value_case = value_proto.WhichOneof("value")
146+
value_mapping = {
147+
"string_value": value_proto.string_value,
148+
"int_value": value_proto.int_value,
149+
"double_value": value_proto.double_value,
150+
"bool_value": value_proto.bool_value,
151+
}
152+
return value_mapping.get(value_case)
153+
154+
155+
def extract_attributes_from_proto(proto_attributes: List[KeyValue]) -> Dict[str, Any]:
156+
"""Extract attributes from protobuf KeyValue pairs."""
157+
attributes = {}
158+
for kv in proto_attributes:
159+
value = extract_attribute_value(kv.value)
160+
if value is not None:
161+
attributes[kv.key] = value
162+
return attributes
163+
164+
165+
class TelemetryServer:
166+
"""Telemetry hopper server that forwards OTLP traces to Azure Monitor."""
167+
168+
def __init__(self, port: int = DEFAULT_GRPC_PORT):
169+
self.port = port
170+
self.server: Optional[grpc.Server] = None
171+
172+
def _start(self) -> None:
173+
"""Start the telemetry forwarding server."""
174+
try:
175+
self.server = self._create_server()
176+
self._setup_signal_handlers()
177+
178+
self.server.start()
179+
logger.info(
180+
f"Telemetry server listening on port {self.port} for OTLP traces"
181+
)
182+
183+
except Exception as e:
184+
logger.error(f"Failed to start server: {e}")
185+
raise
186+
187+
def stop(self, grace_period: int = SHUTDOWN_GRACE_PERIOD_SEC) -> None:
188+
"""Stop the telemetry server gracefully."""
189+
self.server.stop(grace_period)
190+
logger.info("Server stopped")
191+
192+
def wait_for_termination(self) -> None:
193+
self.server.wait_for_termination()
194+
195+
def run(self) -> None:
196+
self._start()
197+
self.wait_for_termination()
198+
199+
def _create_server(self) -> grpc.Server:
200+
server = grpc.server(futures.ThreadPoolExecutor())
201+
trace_service_pb2_grpc.add_TraceServiceServicer_to_server(
202+
TraceServiceHandler(), server
203+
)
204+
server.add_insecure_port(f"[::]:{self.port}")
205+
return server
206+
207+
def _setup_signal_handlers(self) -> None:
208+
209+
def shutdown_handler(signum, frame):
210+
logger.info(f"Received signal {signum}, stopping server")
211+
self.stop()
212+
213+
signal.signal(signal.SIGINT, shutdown_handler)
214+
signal.signal(signal.SIGTERM, shutdown_handler)
215+
216+
217+
if __name__ == "__main__":
218+
server = TelemetryServer()
219+
server.run()

toolkit/tools/go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ require (
1818
go.opentelemetry.io/otel v1.36.0
1919
go.opentelemetry.io/otel/sdk v1.36.0
2020
golang.org/x/sys v0.33.0
21-
gonum.org/v1/gonum v0.16.0
21+
gonum.org/v1/gonum v0.15.0
2222
gopkg.in/alecthomas/kingpin.v2 v2.2.6
2323
gopkg.in/ini.v1 v1.67.0
2424
gopkg.in/yaml.v3 v3.0.1
@@ -36,8 +36,8 @@ require (
3636
github.com/go-logr/logr v1.4.2 // indirect
3737
github.com/go-logr/stdr v1.2.2 // indirect
3838
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
39-
github.com/mailru/easyjson v0.7.7 // indirect
40-
github.com/mattn/go-colorable v0.1.13 // indirect
39+
github.com/mailru/easyjson v0.9.0 // indirect
40+
github.com/mattn/go-colorable v0.1.14 // indirect
4141
github.com/mattn/go-isatty v0.0.20 // indirect
4242
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
4343
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -65,6 +65,7 @@ require (
6565
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
6666
go.opentelemetry.io/otel/trace v1.36.0 // indirect
6767
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
68+
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
6869
golang.org/x/net v0.40.0 // indirect
6970
golang.org/x/text v0.25.0 // indirect
7071
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect

toolkit/tools/go.sum

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
4444
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
4545
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
4646
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
47-
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
4847
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
4948
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
5049
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -55,11 +54,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5554
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5655
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
5756
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
58-
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
59-
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
60-
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
61-
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
62-
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
57+
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
58+
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
59+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
60+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
6361
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
6462
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
6563
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
@@ -141,17 +139,18 @@ go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9f
141139
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
142140
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
143141
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
142+
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
143+
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
144144
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
145145
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
146146
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
147-
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
148147
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
149148
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
150149
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
151150
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
152151
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
153-
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
154-
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
152+
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
153+
gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=
155154
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
156155
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
157156
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=

0 commit comments

Comments
 (0)