Skip to content

Commit

Permalink
Use SeededRandomIdGenerator by default to prevent interference from…
Browse files Browse the repository at this point in the history
… `random.seed` (#457)
  • Loading branch information
alexmojaki authored Sep 27, 2024
1 parent 06ec795 commit e6b0f8c
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 27 deletions.
18 changes: 14 additions & 4 deletions logfire/_internal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
33 changes: 33 additions & 0 deletions logfire/_internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
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

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
Expand Down Expand Up @@ -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
24 changes: 1 addition & 23 deletions logfire/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import random
from dataclasses import dataclass

import pytest
Expand All @@ -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',
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit e6b0f8c

Please sign in to comment.