Skip to content

fix(tracer): flush telemetry when Stop is called - #5345

Open
Hashim1999164 wants to merge 4 commits into
DataDog:mainfrom
Hashim1999164:fix/tracer-stop-flush-telemetry
Open

Hashim1999164 wants to merge 4 commits into
DataDog:mainfrom
Hashim1999164:fix/tracer-stop-flush-telemetry

Conversation

@Hashim1999164

@Hashim1999164 Hashim1999164 commented Sep 10, 2026 •

Copy link
Copy Markdown

Fixes #5249

tracer.Stop was only closing the telemetry client, so the last logs and metrics never flushed.

Stop now calls StopApp when the tracer started a client, which sends app stop, flushes, then closes. If StartApp skipped the tracer client because another product already owned the global client, that leftover still gets Close so the ticker does not leak.

@Hashim1999164
Hashim1999164 requested review from a team as code owners September 10, 2026 21:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 349e457df7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ddtrace/tracer/tracer.go
Comment thread ddtrace/tracer/tracer.go
Comment thread ddtrace/tracer/tracer.go Outdated
Skip StopApp when another product already owns the global client, and flush before closing the log file.
@Hashim1999164
Hashim1999164 force-pushed the fix/tracer-stop-flush-telemetry branch from 805a5c5 to dd3b0a8 Compare September 11, 2026 16:53
@Hashim1999164

Copy link
Copy Markdown
Author

pushed a follow up for the review notes.

StopApp only runs when the tracer actually owns the global telemetry client. if something else owns it we just close the leftover client. also moved the telemetry flush before closing the log file.

on the concurrent Stop race thing, bro i dont know that lmao, left stopOnce as is for now.

@darccio

darccio commented Sep 14, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-14T13:34:41.970101Z dd3b0a8 Manual request
🔒 Security Review ✅ Completed 2026-09-14T13:33:38.949970Z dd3b0a8 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: dd3b0a8e60

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd3b0a8e60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ddtrace/tracer/tracer.go Outdated
StopApp was tearing down the tracer owned global client even after the
profiler had started and reused it. Mark the tracer product stopped,
flush when the profiler is still up, and only StopApp when nothing else
shares the client. Also gate telemetry shutdown with sync.Once so
concurrent Stop calls cannot race on the client pointer.
@Hashim1999164
Hashim1999164 requested a review from a team as a code owner September 18, 2026 12:38
@Hashim1999164

Copy link
Copy Markdown
Author

pushed a follow up for the shared client case. if the profiler is still running we just mark the tracer product stopped and flush, we do not call StopApp. also wrapped the telemetry shutdown in sync.Once so concurrent Stop cannot race on the client pointer

@Hashim1999164

Copy link
Copy Markdown
Author

@darccio any update on this one when you get a chance?

@darccio

darccio commented Sep 28, 2026

Copy link
Copy Markdown
Member

@DataDog review

@darccio

darccio commented Sep 28, 2026

Copy link
Copy Markdown
Member

@Hashim1999164 I'll review it myself this week.

@darccio

darccio commented Sep 28, 2026

Copy link
Copy Markdown
Member

@codex review

Comment thread ddtrace/tracer/tracer.go
Comment on lines +1291 to +1292
// Profiler started after us and still shares this client. Mark the
// tracer product stopped and flush, but leave the app running.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this branch the app is deliberately left running because the profiler still
shares the client, but nothing ever stops it afterwards: profiler.Stop() has no
telemetry teardown at all (no ProductStopped(profilers) and no StopApp anywhere
in the profiler package), so when the profiler is the last product to stop,
app-stopped is never sent and the client's ticker goroutine lives until process
exit. The periodic ticker does keep flushing while the process runs, so this is
bounded (one goroutine + ticker per process), and the gap is pre-existing — in
the profiler-first ordering, main has the same leak. Still worth a TODO here so
the assumption is recorded, with the profiler-side fix as a follow-up PR.

Suggested change
// Profiler started after us and still shares this client. Mark the
// tracer product stopped and flush, but leave the app running.
// Profiler started after us and still shares this client. Mark the
// tracer product stopped and flush, but leave the app running.
// TODO: profiler.Stop() never stops telemetry, so when the profiler is
// the last product to stop, app-stopped is never sent and the client's
// ticker goroutine lives until process exit. Follow-up: profiler.Stop()
// should send ProductStopped(NamespaceProfilers) and StopApp() when it
// owns the global client.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the TODO as suggested

Comment thread ddtrace/tracer/tracer.go
client.Close()
return
}
if traceprof.ProfilerEnabled() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check has a small TOCTOU window against profiler.Start(). SetProfilerEnabled(true)
is the last statement of profiler.Start (profiler/profiler.go:94), after run() has already
called startTelemetry synchronously (profiler/profiler.go:314). If tracer.Stop() executes
in between — profiler's package mu doesn't help, tracer.Stop() never takes it — this
check reads false and StopApp() closes the very client the profiler just registered
ProductStarted(profilers) on, leaving the profiler running with dead telemetry.

This race is pre-existing (main closes the same shared client unconditionally in that
window) and the window is only a handful of statements wide, so not blocking. The cheap
fix is in profiler/profiler.go: set the flag before activeProfiler.run() instead of
after — flush-only is the safe direction during profiler start, since the client
survives and the profiler attaches to it normally. The reverse interleaving is already
fine: if StopApp() completes first, profiler startTelemetry sees GlobalClient() == nil
and starts a fresh app.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raised SetProfilerEnabled(true) before run() so the window is closed

Comment thread ddtrace/tracer/tracer.go
client.Close()
return
}
if traceprof.ProfilerEnabled() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check also trusts a flag that can go stale. In profiler.Start() (profiler/profiler.go:87-89),
when a profiler is already active, the old instance is stopped but neither activeProfiler nor
traceprof.SetProfilerEnabled is reset, and every early return afterwards leaves that state behind:

  • Start() ok, then Start() where the new config is disabled (DD_PROFILING_ENABLED flipped
    in-process; returns nil, so nothing signals a problem): flag stays true with a stopped
    activeProfiler.
  • Start() ok, then Start() failing in newProfiler (agentless without API key,
    AWS_Lambda env, hostname error): same stale state, though at least an error is returned.

In both sequences no profiler is running, but this branch sees ProfilerEnabled() == true,
takes the flush-only path, and leaves the global telemetry client and its ticker goroutine
orphaned until process exit — no app-stopped, and profiler.Stop() afterwards clears the flag
but touches no telemetry. The stale state itself is pre-existing (the branch is unchanged on
main), but this PR is the first consumer that branches on the flag, so main's unconditional
Close() was accidentally correct here while the new check inherits the bug.

The staleness is fixable in five lines, in profiler.Start's stop-old-profiler branch:

if activeProfiler != nil {
    activeProfiler.stop()
    activeProfiler = nil
    traceprof.SetProfilerEnabled(false)
}

That makes the flag strictly track "a profiler is assigned and running" on every early return,
and doesn't widen the (separate) race window between run() and the flag being re-raised. It's
a pre-existing bug so it could be a follow-up PR, but since this change is what makes it
observable, it's worth riding along. No existing test covers Start-then-Start-disabled or
Start-then-Start-error; one should be added with whichever fix lands.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearing activeProfiler + SetProfilerEnabled(false) in the stop-old branch now, plus a restart-disabled test

Comment on lines +257 to +258
assert.False(t, telemetryClient.Stopped)
assert.Equal(t, telemetry.Client(telemetryClient), telemetry.GlobalClient())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions prove StopApp wasn't called on the foreign client — good — but none of the
three tests observe the behavior the PR is named after. assert.Nil(GlobalClient()) in
TestTracerStopFlushesTelemetry proves StopApp ran, not that the queue flushed; and here the
leftover-client Close() (the ticker-leak fix the code comment describes) is unverified,
because RecordClient.Flush() is a no-op (telemetrytest/record.go:216) and Close() records
nothing (record.go:44-46).

RecordClient.Products does record ProductStopped — worth asserting here to pin down that the
tracer's product-stopped still propagates to the foreign-owned client:

Suggested change
assert.False(t, telemetryClient.Stopped)
assert.Equal(t, telemetry.Client(telemetryClient), telemetry.GlobalClient())
// Profiler or another product already owns the global client. Stop must
// not call StopApp on it.
assert.False(t, telemetryClient.Stopped)
assert.Equal(t, telemetry.Client(telemetryClient), telemetry.GlobalClient())
// ProductStopped(tracers) must still propagate to the foreign client
// (ProductStarted set it true during Start; Stop sets it back to false).
assert.False(t, telemetryClient.Products[telemetry.NamespaceTracers])

If you also want the flush and leftover-close directly observable, that needs a small
telemetrytest extension (e.g. Flushes int / Closed bool on RecordClient) — happy to review
that as a follow-up.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the ProductStopped assert. leaving Flushes/Closed on RecordClient for a follow up

Comment on lines +274 to +277
// Profiler started after the tracer and still shares the client, so Stop
// must flush without emitting app-stopped / clearing the global client.
assert.NotNil(t, telemetry.GlobalClient())
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three tests cover tracer-only, foreign-owner, and profiler-still-running, but skip the
fourth ordering: the profiler started after the tracer and then stopped before tracer.Stop()
runs. That's the path where Stop must fall through to StopApp — and it's the one whose
behavior changed (on main the global client survived StopApp-less shutdown, so a
GlobalClient()==nil assertion distinguishes this PR from main). Suggested addition:

Suggested change
// Profiler started after the tracer and still shares the client, so Stop
// must flush without emitting app-stopped / clearing the global client.
assert.NotNil(t, telemetry.GlobalClient())
}
// Profiler started after the tracer and still shares the client, so Stop
// must flush without emitting app-stopped / clearing the global client.
assert.NotNil(t, telemetry.GlobalClient())
}
func TestTracerStopStopsTelemetryAfterProfilerStopped(t *testing.T) {
Start()
defer globalconfig.SetServiceName("")
require.NotNil(t, telemetry.GlobalClient())
wasEnabled := traceprof.SetProfilerEnabled(true)
defer traceprof.SetProfilerEnabled(wasEnabled)
// The profiler started after the tracer, then stopped before tracer.Stop().
traceprof.SetProfilerEnabled(false)
Stop()
// Nobody else needs the client anymore, so Stop must fully stop the app.
assert.Nil(t, telemetry.GlobalClient())
}

Two other gaps I'd leave for a follow-up rather than this PR: the telemetry-disabled path
(client == nil early return) is hard to test because Disabled() caches the env var in a
package-level sync.Once (internal/telemetry/globalclient.go:135-144) — t.Setenv alone won't
work unless the test runs before anything primes the once — and the stale-flag sequences
from my other comment have no coverage either.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added TestTracerStopStopsTelemetryAfterProfilerStopped

Comment thread ddtrace/tracer/tracer.go
}
appsec.Stop()
remoteconfig.Stop()
// Flush telemetry before closing the log file so StopApp diagnostics still land.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is the tracer/profiler ownership handshake for the global telemetry client, but
nothing ties it to the end-state the codebase already anticipates — the TODO at
tracer.go:264-267 ("Will be fixed when the tracer and profiler share control of the global
telemetry client", APMAPI-1771). The current shape — pointer equality plus
traceprof.ProfilerEnabled() — hard-codes a two-product world: appsec already shares the
global client too (appsec.Stop() queues ProductStopped on it), and any future product
sharing it would need another flag or special case here. A refcount in internal/telemetry
(products register on start, StopApp fires when the last one stops) would also dissolve the
flag race and the profiler-never-stops-telemetry gap from my other comments — worth
considering as the follow-up that retires this TODO.

Until then, a pointer from this block to that plan would help the next refactor find it:

Suggested change
// Flush telemetry before closing the log file so StopApp diagnostics still land.
// Flush telemetry before closing the log file so StopApp diagnostics still land.
// Interim tracer/profiler ownership handshake for the global telemetry client;
// to be superseded by shared client control (see TODO at newTracer, APMAPI-1771).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment now points at the newTracer APMAPI-1771 TODO

@Hashim1999164
Hashim1999164 force-pushed the fix/tracer-stop-flush-telemetry branch from 244306a to 2658712 Compare October 2, 2026 20:07
Record the profiler ownership assumptions, clear the enabled flag when a restart leaves no profiler, raise it before run to close the start race, and cover the stop orderings the review called out.
@Hashim1999164
Hashim1999164 force-pushed the fix/tracer-stop-flush-telemetry branch from 2658712 to 0c477b2 Compare October 2, 2026 20:08
@Hashim1999164

Copy link
Copy Markdown
Author

@darccio pushed a pass for your notes.

  • TODO on the profiler-never-stops-telemetry gap
  • pointer to the APMAPI-1771 shared client plan on that shutdown block
  • profiler.Start clears the enabled flag when it stops the old instance, and raises it before run() so the TOCTOU window is closed
  • ProductStopped assert on the foreign client test
  • TestTracerStopStopsTelemetryAfterProfilerStopped for the start-then-stop-then-tracer.Stop ordering
  • restart-disabled profiler test so the stale flag path is covered

happy to do the telemetrytest Flushes/Closed follow up separately if you want that next

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tracer.Stop() never flushes telemetry — pending logs/metrics are silently dropped on shutdown

2 participants