Last active
May 8, 2025 12:20
-
-
Save AlecRosenbaum/5be9729391dfae433f3fb5f8d49ad3a7 to your computer and use it in GitHub Desktop.
Instrumenting The Python Garbage Collector with OpenTelemetry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gc | |
import time | |
from collections import defaultdict | |
import attrs | |
from opentelemetry import metrics | |
from opentelemetry.metrics import CallbackOptions, Observation | |
@attrs.define | |
class GCInstrumenter: | |
# collected synchronously in the callback | |
duration_hist_ns: metrics.Histogram = attrs.field() | |
collected_objs_hist: metrics.Histogram = attrs.field() | |
uncollected_objs_hist: metrics.Histogram = attrs.field() | |
# collected asynchronously w/ observations | |
total_collection_time_by_gen: dict[int, int] = attrs.field( | |
factory=lambda: defaultdict(int) | |
) | |
last_collection_start: int | None = None | |
def callback(self, phase: str, info: dict[str, int]) -> None: | |
if phase == "start": | |
self.last_collection_start = time.monotonic_ns() | |
elif phase == "stop" and self.last_collection_start is not None: | |
now = time.monotonic_ns() | |
duration_ns = now - self.last_collection_start | |
gen = info["generation"] | |
self.total_collection_time_by_gen[gen] += duration_ns | |
self.last_collection_start = None | |
self.collected_objs_hist.record( | |
info["collected"], {"generation": gen} | |
) | |
self.uncollected_objs_hist.record( | |
info["uncollectable"], {"generation": gen} | |
) | |
self.duration_hist_ns.record(duration_ns, {"generation": gen}) | |
def observe_total_gc_time( | |
self, _options: CallbackOptions | |
) -> list[Observation]: | |
return [ | |
Observation(ns / 1e9, attributes={"generation": gen}) | |
for gen, ns in self.total_collection_time_by_gen.items() | |
] | |
def setup_gc_metrics() -> None: | |
meter = metrics.get_meter(__name__) | |
gc_duration_hist = meter.create_histogram( | |
name="cio_gc_duration", | |
description="measures the duration of garbage collection", | |
unit="ns", | |
) | |
gc_collectable_hist = meter.create_histogram( | |
name="cio_gc_collectable_objs", | |
description="The number of collectable objects in each gc run", | |
unit="objects", | |
) | |
gc_uncollectable_hist = meter.create_histogram( | |
name="cio_gc_uncollectable_objs", | |
description="The number of uncollectable objects in each gc run", | |
unit="objects", | |
) | |
gc_instrumenter = GCInstrumenter( | |
duration_hist_ns=gc_duration_hist, | |
collected_objs_hist=gc_collectable_hist, | |
uncollected_objs_hist=gc_uncollectable_hist, | |
) | |
gc.callbacks.append(gc_instrumenter.callback) | |
# Note: observation is implemented as an asynchronous counter so that | |
# we can track the GC time internally in nanoseconds (as ints), but | |
# report observation metrics in seconds (as floats) without losing | |
# precision. | |
meter.create_observable_counter( | |
name="cio_gc_time", | |
description="Time spent doing garbage collection (s)", | |
callbacks=[gc_instrumenter.observe_total_gc_time], | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment