@@ -172,6 +172,59 @@ def my_tracing_endpoint(self) -> Optional[str]:
172
172
provide an *absolute* path to the certificate file instead.
173
173
"""
174
174
175
+
176
+ def _remove_stale_otel_sdk_packages ():
177
+ """Hack to remove stale opentelemetry sdk packages from the charm's python venv.
178
+
179
+ See https://github.com/canonical/grafana-agent-operator/issues/146 and
180
+ https://bugs.launchpad.net/juju/+bug/2058335 for more context. This patch can be removed after
181
+ this juju issue is resolved and sufficient time has passed to expect most users of this library
182
+ have migrated to the patched version of juju. When this patch is removed, un-ignore rule E402 for this file in the pyproject.toml (see setting
183
+ [tool.ruff.lint.per-file-ignores] in pyproject.toml).
184
+
185
+ This only has an effect if executed on an upgrade-charm event.
186
+ """
187
+ # all imports are local to keep this function standalone, side-effect-free, and easy to revert later
188
+ import os
189
+
190
+ if os .getenv ("JUJU_DISPATCH_PATH" ) != "hooks/upgrade-charm" :
191
+ return
192
+
193
+ import logging
194
+ import shutil
195
+ from collections import defaultdict
196
+
197
+ from importlib_metadata import distributions
198
+
199
+ otel_logger = logging .getLogger ("charm_tracing_otel_patcher" )
200
+ otel_logger .debug ("Applying _remove_stale_otel_sdk_packages patch on charm upgrade" )
201
+ # group by name all distributions starting with "opentelemetry_"
202
+ otel_distributions = defaultdict (list )
203
+ for distribution in distributions ():
204
+ name = distribution ._normalized_name # type: ignore
205
+ if name .startswith ("opentelemetry_" ):
206
+ otel_distributions [name ].append (distribution )
207
+
208
+ otel_logger .debug (f"Found { len (otel_distributions )} opentelemetry distributions" )
209
+
210
+ # If we have multiple distributions with the same name, remove any that have 0 associated files
211
+ for name , distributions_ in otel_distributions .items ():
212
+ if len (distributions_ ) <= 1 :
213
+ continue
214
+
215
+ otel_logger .debug (f"Package { name } has multiple ({ len (distributions_ )} ) distributions." )
216
+ for distribution in distributions_ :
217
+ if not distribution .files : # Not None or empty list
218
+ path = distribution ._path # type: ignore
219
+ otel_logger .info (f"Removing empty distribution of { name } at { path } ." )
220
+ shutil .rmtree (path )
221
+
222
+ otel_logger .debug ("Successfully applied _remove_stale_otel_sdk_packages patch. " )
223
+
224
+
225
+ _remove_stale_otel_sdk_packages ()
226
+
227
+
175
228
import functools
176
229
import inspect
177
230
import logging
@@ -197,14 +250,15 @@ def my_tracing_endpoint(self) -> Optional[str]:
197
250
from opentelemetry .sdk .resources import Resource
198
251
from opentelemetry .sdk .trace import Span , TracerProvider
199
252
from opentelemetry .sdk .trace .export import BatchSpanProcessor
200
- from opentelemetry .trace import INVALID_SPAN , Tracer
201
- from opentelemetry .trace import get_current_span as otlp_get_current_span
202
253
from opentelemetry .trace import (
254
+ INVALID_SPAN ,
255
+ Tracer ,
203
256
get_tracer ,
204
257
get_tracer_provider ,
205
258
set_span_in_context ,
206
259
set_tracer_provider ,
207
260
)
261
+ from opentelemetry .trace import get_current_span as otlp_get_current_span
208
262
from ops .charm import CharmBase
209
263
from ops .framework import Framework
210
264
@@ -217,7 +271,7 @@ def my_tracing_endpoint(self) -> Optional[str]:
217
271
# Increment this PATCH version before using `charmcraft publish-lib` or reset
218
272
# to 0 if you are raising the major API version
219
273
220
- LIBPATCH = 11
274
+ LIBPATCH = 14
221
275
222
276
PYDEPS = ["opentelemetry-exporter-otlp-proto-http==1.21.0" ]
223
277
@@ -391,6 +445,9 @@ def wrap_init(self: CharmBase, framework: Framework, *args, **kwargs):
391
445
_service_name = service_name or f"{ self .app .name } -charm"
392
446
393
447
unit_name = self .unit .name
448
+ # apply hacky patch to remove stale opentelemetry sdk packages on upgrade-charm.
449
+ # it could be trouble if someone ever decides to implement their own tracer parallel to
450
+ # ours and before the charm has inited. We assume they won't.
394
451
resource = Resource .create (
395
452
attributes = {
396
453
"service.name" : _service_name ,
@@ -612,38 +669,58 @@ def trace_type(cls: _T) -> _T:
612
669
dev_logger .info (f"skipping { method } (dunder)" )
613
670
continue
614
671
615
- new_method = trace_method (method )
616
- if isinstance (inspect .getattr_static (cls , method .__name__ ), staticmethod ):
672
+ # the span title in the general case should be:
673
+ # method call: MyCharmWrappedMethods.b
674
+ # if the method has a name (functools.wrapped or regular method), let
675
+ # _trace_callable use its default algorithm to determine what name to give the span.
676
+ trace_method_name = None
677
+ try :
678
+ qualname_c0 = method .__qualname__ .split ("." )[0 ]
679
+ if not hasattr (cls , method .__name__ ):
680
+ # if the callable doesn't have a __name__ (probably a decorated method),
681
+ # it probably has a bad qualname too (such as my_decorator.<locals>.wrapper) which is not
682
+ # great for finding out what the trace is about. So we use the method name instead and
683
+ # add a reference to the decorator name. Result:
684
+ # method call: @my_decorator(MyCharmWrappedMethods.b)
685
+ trace_method_name = f"@{ qualname_c0 } ({ cls .__name__ } .{ name } )"
686
+ except Exception : # noqa: failsafe
687
+ pass
688
+
689
+ new_method = trace_method (method , name = trace_method_name )
690
+
691
+ if isinstance (inspect .getattr_static (cls , name ), staticmethod ):
617
692
new_method = staticmethod (new_method )
618
693
setattr (cls , name , new_method )
619
694
620
695
return cls
621
696
622
697
623
- def trace_method (method : _F ) -> _F :
698
+ def trace_method (method : _F , name : Optional [ str ] = None ) -> _F :
624
699
"""Trace this method.
625
700
626
701
A span will be opened when this method is called and closed when it returns.
627
702
"""
628
- return _trace_callable (method , "method" )
703
+ return _trace_callable (method , "method" , name = name )
629
704
630
705
631
- def trace_function (function : _F ) -> _F :
706
+ def trace_function (function : _F , name : Optional [ str ] = None ) -> _F :
632
707
"""Trace this function.
633
708
634
709
A span will be opened when this function is called and closed when it returns.
635
710
"""
636
- return _trace_callable (function , "function" )
711
+ return _trace_callable (function , "function" , name = name )
637
712
638
713
639
- def _trace_callable (callable : _F , qualifier : str ) -> _F :
714
+ def _trace_callable (callable : _F , qualifier : str , name : Optional [ str ] = None ) -> _F :
640
715
dev_logger .info (f"instrumenting { callable } " )
641
716
642
717
# sig = inspect.signature(callable)
643
718
@functools .wraps (callable )
644
719
def wrapped_function (* args , ** kwargs ): # type: ignore
645
- name = getattr (callable , "__qualname__" , getattr (callable , "__name__" , str (callable )))
646
- with _span (f"{ qualifier } call: { name } " ): # type: ignore
720
+ name_ = name or getattr (
721
+ callable , "__qualname__" , getattr (callable , "__name__" , str (callable ))
722
+ )
723
+ with _span (f"{ qualifier } call: { name_ } " ): # type: ignore
647
724
return callable (* args , ** kwargs ) # type: ignore
648
725
649
726
# wrapped_function.__signature__ = sig
0 commit comments