Skip to content

Commit e6b0f8c

Browse files
authored
Use SeededRandomIdGenerator by default to prevent interference from random.seed (#457)
1 parent 06ec795 commit e6b0f8c

File tree

3 files changed

+48
-27
lines changed

3 files changed

+48
-27
lines changed

logfire/_internal/config.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from opentelemetry.sdk.resources import Resource
4747
from opentelemetry.sdk.trace import SpanProcessor, TracerProvider as SDKTracerProvider
4848
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
49-
from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator
49+
from opentelemetry.sdk.trace.id_generator import IdGenerator
5050
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio, Sampler
5151
from opentelemetry.semconv.resource import ResourceAttributes
5252
from rich.console import Console
@@ -83,7 +83,14 @@
8383
from .scrubbing import NOOP_SCRUBBER, BaseScrubber, Scrubber, ScrubbingOptions
8484
from .stack_info import warn_at_user_stacklevel
8585
from .tracer import PendingSpanProcessor, ProxyTracerProvider
86-
from .utils import UnexpectedResponse, ensure_data_dir_exists, get_version, read_toml_file, suppress_instrumentation
86+
from .utils import (
87+
SeededRandomIdGenerator,
88+
UnexpectedResponse,
89+
ensure_data_dir_exists,
90+
get_version,
91+
read_toml_file,
92+
suppress_instrumentation,
93+
)
8794

8895
if TYPE_CHECKING:
8996
from .main import FastLogfireSpan, LogfireSpan
@@ -136,8 +143,11 @@ class AdvancedOptions:
136143
base_url: str = 'https://logfire-api.pydantic.dev'
137144
"""Root URL for the Logfire API."""
138145

139-
id_generator: IdGenerator = dataclasses.field(default_factory=RandomIdGenerator)
140-
"""Generator for trace and span IDs."""
146+
id_generator: IdGenerator = dataclasses.field(default_factory=lambda: SeededRandomIdGenerator(None))
147+
"""Generator for trace and span IDs.
148+
149+
The default generates random IDs and is unaffected by calls to `random.seed()`.
150+
"""
141151

142152
ns_timestamp_generator: Callable[[], int] = time.time_ns
143153
"""Generator for nanosecond start and end timestamps of spans."""

logfire/_internal/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
import json
55
import logging
66
import os
7+
import random
78
import sys
89
from contextlib import contextmanager
10+
from dataclasses import dataclass
911
from pathlib import Path
1012
from types import TracebackType
1113
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Sequence, Tuple, TypedDict, TypeVar, Union
1214

1315
from opentelemetry import context, trace as trace_api
1416
from opentelemetry.sdk.resources import Resource
1517
from opentelemetry.sdk.trace import Event, ReadableSpan
18+
from opentelemetry.sdk.trace.id_generator import IdGenerator
1619
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
1720
from opentelemetry.trace.status import Status
1821
from opentelemetry.util import types as otel_types
@@ -348,3 +351,33 @@ def maybe_capture_server_headers(capture: bool):
348351

349352
def is_asgi_send_receive_span_name(name: str) -> bool:
350353
return name.endswith((' http send', ' http receive', ' websocket send', ' websocket receive'))
354+
355+
356+
@dataclass(repr=True)
357+
class SeededRandomIdGenerator(IdGenerator):
358+
"""Generate random span/trace IDs from a seed for deterministic tests.
359+
360+
Similar to RandomIdGenerator from OpenTelemetry, but with a seed.
361+
Set the seed to None for non-deterministic randomness.
362+
In that case the difference from RandomIdGenerator is that it's not affected by `random.seed(...)`.
363+
364+
Trace IDs are 128-bit integers.
365+
Span IDs are 64-bit integers.
366+
"""
367+
368+
seed: int | None = 0
369+
370+
def __post_init__(self) -> None:
371+
self.random = random.Random(self.seed)
372+
373+
def generate_span_id(self) -> int:
374+
span_id = self.random.getrandbits(64)
375+
while span_id == trace_api.INVALID_SPAN_ID: # pragma: no cover
376+
span_id = self.random.getrandbits(64)
377+
return span_id
378+
379+
def generate_trace_id(self) -> int:
380+
trace_id = self.random.getrandbits(128)
381+
while trace_id == trace_api.INVALID_TRACE_ID: # pragma: no cover
382+
trace_id = self.random.getrandbits(128)
383+
return trace_id

logfire/testing.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from __future__ import annotations
44

5-
import random
65
from dataclasses import dataclass
76

87
import pytest
@@ -14,6 +13,7 @@
1413

1514
from ._internal.constants import ONE_SECOND_IN_NANOSECONDS
1615
from ._internal.exporters.test import TestExporter
16+
from ._internal.utils import SeededRandomIdGenerator
1717

1818
__all__ = [
1919
'capfire',
@@ -56,28 +56,6 @@ def generate_trace_id(self) -> int:
5656
return self.trace_id_counter
5757

5858

59-
@dataclass(repr=True)
60-
class SeededRandomIdGenerator(IdGenerator):
61-
"""Generate random span/trace IDs from a random seed for deterministic tests.
62-
63-
Trace IDs are 64-bit integers.
64-
Span IDs are 32-bit integers.
65-
"""
66-
67-
seed: int = 0
68-
69-
def __post_init__(self) -> None:
70-
self.random = random.Random(self.seed)
71-
72-
def generate_span_id(self) -> int:
73-
"""Generates a random span id."""
74-
return self.random.getrandbits(64)
75-
76-
def generate_trace_id(self) -> int:
77-
"""Generates a random trace id."""
78-
return self.random.getrandbits(128)
79-
80-
8159
# Making this a dataclass causes errors in the process pool end-to-end tests
8260
class TimeGenerator:
8361
"""Generate incrementing timestamps for testing.

0 commit comments

Comments
 (0)