Skip to content

Use OS entropy in RandomIdGenerator so random.seed() cannot repeat trace/span IDs - #5590

Open
geekflyer wants to merge 1 commit into
open-telemetry:mainfrom
geekflyer:fix-id-generator-global-random-seed
Open

Use OS entropy in RandomIdGenerator so random.seed() cannot repeat trace/span IDs#5590
geekflyer wants to merge 1 commit into
open-telemetry:mainfrom
geekflyer:fix-id-generator-global-random-seed

Conversation

@geekflyer

@geekflyer geekflyer commented Aug 27, 2026

Copy link
Copy Markdown

Description

Fixes #4376

RandomIdGenerator — the default ID generator every TracerProvider uses — draws trace and span IDs from the process-global random module:

span_id = random.getrandbits(64)
trace_id = random.getrandbits(128)

Any application that calls random.seed(...) therefore fixes the exact sequence of trace and span IDs the SDK will generate. Seeding the global generator is standard practice for reproducibility in machine-learning and scientific workloads, and those applications get identical trace and span IDs across process restarts and across replicas — separate runs silently overwrite or corrupt each other's traces in the backend.

This PR makes RandomIdGenerator draw IDs from operating-system entropy via secrets.randbits() instead. OS entropy cannot be seeded from Python and is not copied across fork(), so seeding can no longer make separate processes deterministically repeat IDs — and pre-forking servers (e.g. Gunicorn) no longer have workers that inherit identical generator state.

Reproduction

import random
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator

random.seed(10)  # e.g. for ML reproducibility
print(hex(RandomIdGenerator().generate_trace_id()))

Before this change, every process running this prints the same "random" ID:

0x7b855e7e130e5b5b171b1a0810a71a53   (= 164207228320579316746596838417247989971)

which is byte-for-byte the duplicate trace ID reported in #4376. Two eval jobs, two service replicas, or one service before/after a restart all emit spans under the same trace ID.

How other implementations handle this

The other major OTel SDKs are already immune to application seeding, and the most prominent downstream Python consumer has shipped a workaround for this SDK:

Why secrets and not a private random.Random()

A private random.Random() instance (as proposed in #4377) fixes the random.seed() case but keeps two failure modes:

  • fork() duplication — a Mersenne Twister instance created before fork() (preforking servers, multiprocessing with the fork start method) is copied byte-for-byte into every child, and all children then generate the same ID sequence.
  • The instance remains reachable, so test frameworks or user code can still seed it by accident.

MLflow's workaround above is the working proof of this trade-off: because it uses a private random.Random(), it must also maintain a registry of every generator instance and re-seed them all via os.register_at_fork(after_in_child=...) to avoid forked children inheriting identical state (mlflow/tracing/provider.py @ 948322ac, L76–L87).

secrets.randbits() reads from the OS CSPRNG (os.urandom), which is seedless and stateless per-read, so both problems disappear with a one-line change and no fork hooks. It also keeps is_trace_id_random() honest: previously the SDK set the W3C random-trace-id flag while emitting fully deterministic IDs under a seeded generator. All 128/64 bits remain uniformly random, as TraceIdRatioBased-style samplers require.

Performance: measured with timeit on CPython 3.12 (Linux x86_64): secrets.randbits(64) ≈ 546 ns/call vs random.getrandbits(64) ≈ 59 ns/call, and secrets.randbits(128) ≈ 549 ns — about 0.5 µs extra per generated ID (one getrandom(2) syscall). Span creation performs sampling, attribute handling, context operations, and clock reads that cost tens of µs in this SDK; at 10 000 spans/s the added cost is ~10 ms/s of one core.

Changes

File Change
opentelemetry-sdk/src/opentelemetry/sdk/trace/id_generator.py random.getrandbitssecrets.randbits; docstring documents the entropy source and fork behavior
opentelemetry-sdk/tests/trace/test_trace.py Invalid-ID tests patch opentelemetry.sdk.trace.id_generator.secrets.randbits (mock-where-used) and assert exact call lists; new regression test test_ids_unaffected_by_global_random_seed
opentelemetry-sdk/tests/trace/composite_sampler/test_traceid_ratio.py test_sampling generates trace IDs from a locally seeded random.Random(0) instead of RandomIdGenerator
.changelog/<PR>.fixed Changelog fragment (includes the behavior-change note)

Why the sampler test change is needed

opentelemetry-sdk/tests/conftest.py installs an autouse fixture that calls random.seed(0) before every test — the SDK's own test suite relied on the seedable ID generator to make its statistical assertions deterministic. test_sampling asserts a sampled count within ±50 of the expectation over 10 000 IDs; at ratio 0.45 that tolerance is about one standard deviation, so with genuinely random IDs the test would fail roughly 30% of the time. Seeding a local random.Random(0) for the test's ID stream keeps it deterministic, and matches the ID stream the test consumed before this change (the global random functions delegate to a hidden Random instance with identical seeding). The test exercises the sampler, not the ID generator, so coverage is unchanged.

Behavior change note

If an application relied on random.seed() to obtain reproducible trace/span IDs (snapshot tests, demos), that stops working — deliberately, since IDs are wire-level identifiers whose uniqueness is what backends key on. Deterministic IDs remain available by passing a custom IdGenerator to TracerProvider(id_generator=...). This is stated in the changelog fragment.

Prior art / history

Possible follow-up (contrib repo): AwsXRayIdGenerator in opentelemetry-python-contrib builds its random bits from the same global random module and appears to inherit the same defect; worth a linked issue there once this merges.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • New TestRandomIdGenerator.test_ids_unaffected_by_global_random_seed fails against the previous implementation — reproducing When the random seed is set, it causes duplicate traceId and spanId. #4376's exact duplicate IDs (164207228320579316746596838417247989971 / 273610340023782072 under seed(10)) — and passes with this change.
  • Full opentelemetry-sdk unit-test suite run before and after: identical results except the added test.
  • Focused suites (tests/trace/test_trace.py, tests/trace/composite_sampler/, tests/trace/test_sampling.py) run repeatedly with no flakes.
  • Microbenchmark of ID-generation cost (numbers above).

Does This PR Require a Contrib Repo Change?

  • No. (See the possible follow-up note above; not required for correctness of this change.)

Checklist:

  • Followed the style guidelines of this project
  • Changelog fragment added (.changelog/<PR>.fixed)
  • Unit tests have been added
  • Documentation has been updated (docstring)

@geekflyer
geekflyer requested a review from a team as a code owner August 27, 2026 06:04
@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 27, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: geekflyer / name: Christian Theilemann (8bcbfea)

@geekflyer geekflyer changed the title Use OS entropy for trace and span ID generation Use OS entropy in RandomIdGenerator so random.seed() cannot repeat trace/span IDs Aug 27, 2026
RandomIdGenerator drew IDs from the process-global random module, so
applications that call random.seed() for reproducibility (common in ML
workloads) made separate processes and restarts generate identical
trace and span IDs, silently corrupting or overwriting traces.

Draw IDs from secrets.randbits() (OS entropy) instead: unaffected by
random.seed(), and not copied across fork(). Decouple the statistical
composite-sampler test from the conftest random_seed fixture by seeding
a local generator that reproduces the identical ID stream.

Fixes open-telemetry#4376
@geekflyer
geekflyer force-pushed the fix-id-generator-global-random-seed branch from 04ae8f3 to 8bcbfea Compare August 27, 2026 06:06
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Aug 27, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on reviewers · refreshed 2026-08-28 21:09 UTC

Review the latest changes.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@ocelotl

ocelotl commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The changes in this PR can have impact on performance, please take a look here.

@ocelotl

ocelotl commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

If I understand this correctly:

There is seeding mechanism A which is currently being used in the SDK. Using mechanism A in the SDK causes problems because there are many applications that also use A.
There is seeding mechanism B, which this PR introduces. Not many applications use B so this is not a problem.

But what about the applications that use mechanism B? Would that introduce the same problem as using mechanism A? I understand that maybe it is impossible not to affect any application so that we have to choose between A and B and B can be the one who affects applications less.

@ocelotl

ocelotl commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Seeding is something that we need for testing. Every test run must be seeded so that we can get reproducible results. I think the changes in this PR will break this. I am ok with using a better seeding mechanism if that is advantageous but we gotta find the way of keeping our tests also seeded.

@geekflyer

Copy link
Copy Markdown
Author

The changes in this PR can have impact on performance, please take a look here.

I'm aware, but I think they're acceptable, especially considering that other SDKs like Go and co. go through the same path

@geekflyer

Copy link
Copy Markdown
Author

If I understand this correctly:

There is seeding mechanism A which is currently being used in the SDK. Using mechanism A in the SDK causes problems because there are many applications that also use A. There is seeding mechanism B, which this PR introduces. Not many applications use B so this is not a problem.

But what about the applications that use mechanism B? Would that introduce the same problem as using mechanism A? I understand that maybe it is impossible not to affect any application so that we have to choose between A and B and B can be the one who affects applications less.

the issue with mechanism A is that it can be set to a fixed seed (shared state if you will) that leads to ID conflicts when doing any sort of multi-threading. Mechanism B doesn't have a way to set a fixed seed, so no matter how many applications use mechanism B, they won't interfere which each other.

@geekflyer

Copy link
Copy Markdown
Author

Seeding is something that we need for testing. Every test run must be seeded so that we can get reproducible results. I think the changes in this PR will break this. I am ok with using a better seeding mechanism if that is advantageous but we gotta find the way of keeping our tests also seeded.

The PR already addresses this. https://github.com/open-telemetry/opentelemetry-python/pull/5590/changes#diff-09cc59b650f6838fc3236c1ad85d8af81ea8b0a23a428a5f9ad095345d7ff6e4R64 injects a fixed seed random generator instead of using the default one.

@ocelotl

ocelotl commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Seeding is something that we need for testing. Every test run must be seeded so that we can get reproducible results. I think the changes in this PR will break this. I am ok with using a better seeding mechanism if that is advantageous but we gotta find the way of keeping our tests also seeded.

The PR already addresses this. https://github.com/open-telemetry/opentelemetry-python/pull/5590/changes#diff-09cc59b650f6838fc3236c1ad85d8af81ea8b0a23a428a5f9ad095345d7ff6e4R64 injects a fixed seed random generator instead of using the default one.

What I mean is that for our tests we need to do seeding at one single place and that should do the seeding that makes every single test in the run reproducible. For example, having that seeding mechanism in a conftest.py file that applies to all tests, so that we can develop tests and not worry about having to do seeding for every test.

I have not checked yet, we may not have that mechanism in place and if not this is a different issue.

@ocelotl

ocelotl commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Mechanism B doesn't have a way to set a fixed seed, so no matter how many applications use mechanism B, they won't interfere which each other.

Hmm, if mechanism B does not have a way to set a fixed seed, how do we set the fixed seed we need for testing? 🤔

@geekflyer

Copy link
Copy Markdown
Author

Seeding is something that we need for testing. Every test run must be seeded so that we can get reproducible results. I think the changes in this PR will break this. I am ok with using a better seeding mechanism if that is advantageous but we gotta find the way of keeping our tests also seeded.

The PR already addresses this. https://github.com/open-telemetry/opentelemetry-python/pull/5590/changes#diff-09cc59b650f6838fc3236c1ad85d8af81ea8b0a23a428a5f9ad095345d7ff6e4R64 injects a fixed seed random generator instead of using the default one.

What I mean is that for our tests we need to do seeding at one single place and that should do the seeding that makes every single test in the run reproducible. For example, having that seeding mechanism in a conftest.py file that applies to all tests, so that we can develop tests and not worry about having to do seeding for every test.

I have not checked yet, we may not have that mechanism in place and if not this is a different issue.

I don't think that mechanism is in place yet and currently nothing other than conftest.py relies on seeded randomness, otherwise my change would've already broken many tests.

@geekflyer

geekflyer commented Aug 28, 2026

Copy link
Copy Markdown
Author

Mechanism B doesn't have a way to set a fixed seed, so no matter how many applications use mechanism B, they won't interfere which each other.

Hmm, if mechanism B does not have a way to set a fixed seed, how do we set the fixed seed we need for testing? 🤔

well, to be more precise, the new code makes otel-python use by default a randomness source that CANNOT be altered by anyone, including otel-python itself or other unrelated application code. However one can still pass another id generator that uses a different (deterministic) randomness source to TracerProvider, which is basically what you'd do in tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

When the random seed is set, it causes duplicate traceId and spanId.

2 participants