From e6b0f8c3ae8b1f9e8355ba3bdf0e0b71d638d15f Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 27 Sep 2024 13:59:52 +0200 Subject: [PATCH] Use `SeededRandomIdGenerator` by default to prevent interference from `random.seed` (#457) --- logfire/_internal/config.py | 18 ++++++++++++++---- logfire/_internal/utils.py | 33 +++++++++++++++++++++++++++++++++ logfire/testing.py | 24 +----------------------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/logfire/_internal/config.py b/logfire/_internal/config.py index 866af53c1..be01e53a7 100644 --- a/logfire/_internal/config.py +++ b/logfire/_internal/config.py @@ -46,7 +46,7 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import SpanProcessor, TracerProvider as SDKTracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor -from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator +from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio, Sampler from opentelemetry.semconv.resource import ResourceAttributes from rich.console import Console @@ -83,7 +83,14 @@ from .scrubbing import NOOP_SCRUBBER, BaseScrubber, Scrubber, ScrubbingOptions from .stack_info import warn_at_user_stacklevel from .tracer import PendingSpanProcessor, ProxyTracerProvider -from .utils import UnexpectedResponse, ensure_data_dir_exists, get_version, read_toml_file, suppress_instrumentation +from .utils import ( + SeededRandomIdGenerator, + UnexpectedResponse, + ensure_data_dir_exists, + get_version, + read_toml_file, + suppress_instrumentation, +) if TYPE_CHECKING: from .main import FastLogfireSpan, LogfireSpan @@ -136,8 +143,11 @@ class AdvancedOptions: base_url: str = 'https://logfire-api.pydantic.dev' """Root URL for the Logfire API.""" - id_generator: IdGenerator = dataclasses.field(default_factory=RandomIdGenerator) - """Generator for trace and span IDs.""" + id_generator: IdGenerator = dataclasses.field(default_factory=lambda: SeededRandomIdGenerator(None)) + """Generator for trace and span IDs. + + The default generates random IDs and is unaffected by calls to `random.seed()`. + """ ns_timestamp_generator: Callable[[], int] = time.time_ns """Generator for nanosecond start and end timestamps of spans.""" diff --git a/logfire/_internal/utils.py b/logfire/_internal/utils.py index 9a59a1a8b..dfec9a2f2 100644 --- a/logfire/_internal/utils.py +++ b/logfire/_internal/utils.py @@ -4,8 +4,10 @@ import json import logging import os +import random import sys from contextlib import contextmanager +from dataclasses import dataclass from pathlib import Path from types import TracebackType from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Sequence, Tuple, TypedDict, TypeVar, Union @@ -13,6 +15,7 @@ from opentelemetry import context, trace as trace_api from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import Event, ReadableSpan +from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.trace.status import Status from opentelemetry.util import types as otel_types @@ -348,3 +351,33 @@ def maybe_capture_server_headers(capture: bool): def is_asgi_send_receive_span_name(name: str) -> bool: return name.endswith((' http send', ' http receive', ' websocket send', ' websocket receive')) + + +@dataclass(repr=True) +class SeededRandomIdGenerator(IdGenerator): + """Generate random span/trace IDs from a seed for deterministic tests. + + Similar to RandomIdGenerator from OpenTelemetry, but with a seed. + Set the seed to None for non-deterministic randomness. + In that case the difference from RandomIdGenerator is that it's not affected by `random.seed(...)`. + + Trace IDs are 128-bit integers. + Span IDs are 64-bit integers. + """ + + seed: int | None = 0 + + def __post_init__(self) -> None: + self.random = random.Random(self.seed) + + def generate_span_id(self) -> int: + span_id = self.random.getrandbits(64) + while span_id == trace_api.INVALID_SPAN_ID: # pragma: no cover + span_id = self.random.getrandbits(64) + return span_id + + def generate_trace_id(self) -> int: + trace_id = self.random.getrandbits(128) + while trace_id == trace_api.INVALID_TRACE_ID: # pragma: no cover + trace_id = self.random.getrandbits(128) + return trace_id diff --git a/logfire/testing.py b/logfire/testing.py index cb121d881..5f1650c90 100644 --- a/logfire/testing.py +++ b/logfire/testing.py @@ -2,7 +2,6 @@ from __future__ import annotations -import random from dataclasses import dataclass import pytest @@ -14,6 +13,7 @@ from ._internal.constants import ONE_SECOND_IN_NANOSECONDS from ._internal.exporters.test import TestExporter +from ._internal.utils import SeededRandomIdGenerator __all__ = [ 'capfire', @@ -56,28 +56,6 @@ def generate_trace_id(self) -> int: return self.trace_id_counter -@dataclass(repr=True) -class SeededRandomIdGenerator(IdGenerator): - """Generate random span/trace IDs from a random seed for deterministic tests. - - Trace IDs are 64-bit integers. - Span IDs are 32-bit integers. - """ - - seed: int = 0 - - def __post_init__(self) -> None: - self.random = random.Random(self.seed) - - def generate_span_id(self) -> int: - """Generates a random span id.""" - return self.random.getrandbits(64) - - def generate_trace_id(self) -> int: - """Generates a random trace id.""" - return self.random.getrandbits(128) - - # Making this a dataclass causes errors in the process pool end-to-end tests class TimeGenerator: """Generate incrementing timestamps for testing.