Skip to content

skill(apm-integrations): additional rules from recent HTTP-category PR reviews#11927

Open
jordan-wong wants to merge 5 commits into
masterfrom
feat/skill-http-followup-rules
Open

skill(apm-integrations): additional rules from recent HTTP-category PR reviews#11927
jordan-wong wants to merge 5 commits into
masterfrom
feat/skill-http-followup-rules

Conversation

@jordan-wong

@jordan-wong jordan-wong commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the add-apm-integrations skill with additional rules derived from reviewer feedback and CI-fix commits across recent HTTP-category PRs (#11708 sparkjava-2.3, #11709 feign-10.8, #11717 commons-httpclient-2.0). Also reconciles a conflict between two maintainer positions on super(...) naming.

Follow-on to #11760.

What's added

Rules from reviewer comments (in the referenced PRs):

Area Rule Source
Async wrapping (advice-class.md) Do not open a span on an async wrapper when the sync delegate is already instrumented — produces two spans per request @ValentinZakharov on #11709
Helper anti-pattern (advice-class.md) Do not extract advice logic into a per-instrumentation helper just to shorten the advice body — inline unless genuinely shared @ygree on #11717
Framework-in-framework (advice-class.md) When an inner framework (Spark routes, JAX-RS handlers) runs inside an outer HTTP server (Jetty/Netty/Undertow), enrich the active span with HTTP_RESOURCE_DECORATOR.withRoute(...) — do NOT create a new span, Decorator, or AgentScope. Null-check activeSpan() first. @codex on #11708
CompletableFuture (context-tracking.md) Do not reassign future = future.whenComplete(...)whenComplete produces a dependent stage, and cancelling that stage does not cancel the original request @codex on #11709
Test hygiene (tests.md) No Thread.sleep(); embedded servers as static fields, not per-test; shared scaffolding in a base class; ForkedTest requires a justified isolation reason; no default jvmArgs in test tasks @PerfectSlayer on #11708
Module preservation (instrumenter-module.md) When rewriting or refactoring an existing module, preserve every override from master (super(...), defaultEnabled(), helperClassNames(), contextStore(), orderPriority(), muzzleDirective()) — silent loss of defaultEnabled()=false on an opt-in integration ships it on by default Recurring theme across #11708 / #11709
Duplicate modules (instrumenter-module.md) Before creating dd-java-agent/instrumentation/$framework/$framework-$version/, list the parent directory to check for existing coverage; do not create parallel duplicate modules @PerfectSlayer historically on #10941
testImplementation version (muzzle.md) Default to matching the module's declared minimum; document justified deviations (e.g. sparkjava-2.3's compile-against-2.3 / test-against-2.4 with a build.gradle header comment) @PerfectSlayer on #11708

Rules from CI-fix commit history on #11708:

Area Rule
advice-class.md No non-constant static fields in *Advice.java (muzzle contract — advice methods are inlined and non-constant statics get pulled into every callsite)
supported-configurations.md Registry uses canonical type names — boolean, int (NOT integer), decimal (NOT double), string, array, map. Every super(...) name needs a matching _ENABLED entry; every _ANALYTICS_SAMPLE_RATE needs a matching _ANALYTICS_ENABLED.
instrumenter-module.md Any helper class called from advice must be declared in helperClassNames() — otherwise the target classloader lacks it and the advice throws NoClassDefFoundError at runtime

super(...) naming reconciliation (instrumenter-module.md)

Two maintainer positions were in tension:

Empirical check of the codebase: single-name is the norm even for versioned modules — freemarker-2.3.9 + freemarker-2.3.24 both super("freemarker"); liberty-20.0 + liberty-23.0 both super("liberty"). The multi-name form appears when frameworks have real cross-version group switches: okhttp (super("okhttp", "okhttp-3") because okhttp-2.0/ sibling exists).

Also worth noting: the prior skill example for the version-alias pattern used super("jedis", "jedis-3.0"), but the shipping jedis-3.0 module actually uses super("jedis", "redis"), and DD_TRACE_JEDIS_3_0_ENABLED does not exist in metadata/supported-configurations.json.

Resolution: encode as conditional guidance — pass ONE name unless multiple version-specific sibling directories genuinely exist. sparkjava-2.3's super("sparkjava", "sparkjava-2.4") is included as a nuanced counter-example (the -2.4 alias reflects the version exercised by tests, not the directory name — see the build.gradle header comment on that module).

Both maintainer positions are correct in their respective situations; the reconciled guidance in this PR distinguishes them explicitly.

Files changed

  • .agents/skills/apm-integrations/references/advice-class.md
  • .agents/skills/apm-integrations/references/context-tracking.md
  • .agents/skills/apm-integrations/references/instrumenter-module.md
  • .agents/skills/apm-integrations/references/muzzle.md
  • .agents/skills/apm-integrations/references/supported-configurations.md
  • .agents/skills/apm-integrations/references/tests.md

Not in scope

Five rules distilled from HTTP-client and HTTP-server PR reviews
(feign #11709, commons-httpclient #11717, sparkjava #11708) that were
NOT covered by the R13-R33 additions in #11760.

- advice-class.md: async double-span (do not wrap an async client with
  advice when the sync delegate is already instrumented; produces two
  spans per request). Source: @ValentinZakharov on PR #11709.
- advice-class.md: HelperMethods refactor anti-pattern (do not extract
  advice logic into a per-instrumentation helper class just to shorten
  the advice body — inline unless genuinely shared). Source: @ygree on
  PR #11717.
- instrumenter-module.md: regeneration must preserve every override the
  master version has — not just super(...), but also defaultEnabled(),
  helperClassNames(), contextStore(), orderPriority(), etc. Source:
  Codex reviews on PRs #11709 and #11708 (recurring pattern).
- instrumenter-module.md: scan dd-java-agent/instrumentation/$framework/
  before generating; do not create parallel duplicate modules. Source:
  @PerfectSlayer's historical review on #10941, still an unencoded gap.
- muzzle.md: base testImplementation dep version must match the module's
  declared minimum, not the latest — extends the existing latestDep
  parity rule to base tests. Source: @PerfectSlayer on PR #11708.

3 files, +44 lines. No changes to existing rules.

Reviewers: @mcculls (skill hygiene, reviewed #11760), @PerfectSlayer
(HTTP domain, contributed rules from #10941 + #11708).
Adds 9 rules across 5 files based on extended audit of PR #11708 (sparkjava),
PR #11709 (feign), and the sparkjava CI-fix commit history:

- context-tracking: CompletableFuture cancellation preservation (codex, #11709)
- advice-class: framework-inside-framework span identity (codex, #11708)
- advice-class: no non-constant static fields in advice (CI-fix commit f6d1263)
- tests: 5 test-hygiene rules (PerfectSlayer, #11708) — no Thread.sleep, static
  server field, shared test bases, ForkedTest justification, no default jvmArgs
- supported-configurations: registry type correctness (CI-fix commits 53836a0, 48a84c0)
- instrumenter-module: helperClassNames() for enrichment helpers (CI-fix commit 2c372a3)
…ture

Reconciles two conflicting maintainer positions on the constructor pattern:
- Stuart McCulloch (PR #11760 comment 3552616459) — new instrumentations
  should adopt the version-alias pattern
- Valentin Zakharov (PR #11709 comment 3532152120) — single-module frameworks
  should pass one name; extra names mint DD_TRACE_<NAME>_ENABLED flags with
  no counterpart to gate against

The empirical convention in dd-trace-java confirms Valentin's rule for
single-module frameworks (feign, freemarker, liberty, sparkjava all pass one
name; freemarker and liberty do so even with real version siblings) and
McCulloch's rule for frameworks with real sibling versions (okhttp shipping
okhttp-2.0 and okhttp-3.0 with a shared 'okhttp' group flag).

Also fixes the previous jedis example which claimed 'jedis-3.0' alias but
the shipping jedis-3.0 module actually passes super("jedis", "redis").
@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Jul 13, 2026

Copy link
Copy Markdown

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 57.04% (+0.00%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 04a0719 | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.73 s 14.72 s [-0.7%; +0.8%] (no difference)
startup:insecure-bank:tracing:Agent 13.59 s 13.69 s [-1.4%; -0.1%] (maybe better)
startup:petclinic:appsec:Agent 16.35 s 16.81 s [-7.2%; +1.7%] (no difference)
startup:petclinic:iast:Agent 16.89 s 17.02 s [-1.4%; -0.1%] (maybe better)
startup:petclinic:profiling:Agent 16.32 s 16.87 s [-7.6%; +1.1%] (no difference)
startup:petclinic:sca:Agent 16.93 s 16.82 s [-0.3%; +1.7%] (no difference)
startup:petclinic:tracing:Agent 16.13 s 16.14 s [-1.0%; +0.9%] (no difference)

Commit: 04a07191 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@jordan-wong

jordan-wong commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Behaviour check — how the current draft affects generated output

Update (2026-07-13 pm) — original findings retracted. After posting this comment I extracted the compile-step agent trace and discovered the blind regens did NOT actually load the draft skill content in this PR. The toolkit's skill-loader first-match preferred the target repo's in-tree .claude/skills/apm-integrations/ (a symlink to the merged, pre-#11927 .agents/skills/ on the target checkout) over the toolkit-bundled copy that was synced from this branch. So every "rule was in the skill and the model violated it" claim below is wrong — the agent was faithfully following the older pre-#11927 rules, which say the OPPOSITE of what this PR proposes for super(...) naming.

Preserving the original observations below for the record, but with corrections:

Two candidates:

  • commons-httpclient-2.0 (existing on master)
  • sparkjava-2.3 (existing on master)

Static-verification results — corrected classification:

Rule (in this PR) Original claim Corrected classification
Preserve overrides from master (narrow super(...) verbatim) Both FAILED Real failure (commons + sparkjava, narrow case) — Jul-10 rule DID say copy super(...) verbatim; commons added -2.0 alias, sparkjava changed -2.4 to -2.3
Preserve overrides (broader: defaultEnabled, muzzle range, group flag) Both FAILED Not tested — this broader rule was only added in this PR draft; the Jul-10 skill had only the narrow super(...) clause
super(...) single-name for single-module frameworks (F2) Both FAILED Retracted — Jul-10 rule said the OPPOSITE (super("jedis", "jedis-3.0") as CORRECT for new modules). Output is textbook compliance with Jul-10
Framework-inside-framework: enrich, do not replace (S1) Sparkjava FAILED critically Retracted — S1 was drafted in this PR from @codex's #11708 review. Jul-10 skill had no such guidance.
metadata/supported-configurations.json correctness Sparkjava FAILED (dropped group flag) Partial — general rule existed in a Jul-10 reference file the agent didn't Read; specific per-name _ENABLED requirement was only in this PR draft
No non-constant static fields in *Advice.java Both PASSED Passed (rule wasn't in Jul-10 either — the output happens to be correct)
HelperMethods anti-pattern (no wrapper helpers) Both PASSED Passed (rule wasn't in Jul-10 either)

Corrected reading: the rule content in this PR is unchanged and still worth reviewing on its merits. The empirical validation evidence in my original comment is retracted pending a re-run against a properly-loaded skill. I'm setting up that re-run now on an async-category cycle (rxjava-3.0) with the skill correctly loaded via a target-repo branch that has the draft in-tree; results will follow if useful.

Sorry for the noise on the earlier reading.

Copilot AI 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.

Pull request overview

Extends the canonical apm-integrations skill reference docs with additional rules extracted from recent HTTP-category review/CI feedback, and refines guidance around integration naming via super(...) based on version-sibling structure.

Changes:

  • Adds “test hygiene” rules for instrumentation tests (deterministic waits, embedded server lifecycle, base-class factoring, forked-test justification, avoiding default JVM args).
  • Adds registry population guidance for metadata/supported-configurations.json (required _ENABLED entries, analytics key shapes, common mistakes).
  • Expands integration authoring guidance around module discovery, super(...) naming choices, async wrapper double-span avoidance, CompletableFuture cancellation preservation, and advice-class constraints.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
.agents/skills/apm-integrations/references/tests.md Adds test-hygiene rules intended to reduce flakiness and CI cost.
.agents/skills/apm-integrations/references/supported-configurations.md Adds common-mistake guidance for correctly registering new configuration entries.
.agents/skills/apm-integrations/references/muzzle.md Adds a rule tying test dependency versions to declared module ranges/min versions.
.agents/skills/apm-integrations/references/instrumenter-module.md Adds “scan first” guidance, reconciles super(...) naming rules, and adds helper declaration guidance.
.agents/skills/apm-integrations/references/context-tracking.md Adds guidance to preserve cancellation semantics for async return types.
.agents/skills/apm-integrations/references/advice-class.md Adds rules for async double-span avoidance, helper-method anti-pattern, framework-in-framework enrichment, and advice static-field constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .agents/skills/apm-integrations/references/tests.md Outdated
Comment thread .agents/skills/apm-integrations/references/supported-configurations.md Outdated
Comment thread .agents/skills/apm-integrations/references/instrumenter-module.md Outdated
Comment thread .agents/skills/apm-integrations/references/instrumenter-module.md Outdated
Comment thread .agents/skills/apm-integrations/references/muzzle.md Outdated
Comment thread .agents/skills/apm-integrations/references/context-tracking.md Outdated
Comment thread .agents/skills/apm-integrations/references/advice-class.md
7 findings, all verified against master:

- tests.md: waitForTraces(N) waits for >=N (not exactly N) with 20s
  bounded timeout per ListWriter.java
- supported-configurations.md: fix internal contradiction — registry
  uses "decimal" (rates) and "int" (counts), NOT "double"/"integer"
  (matches canonical guidance earlier in same doc, line 52)
- instrumenter-module.md: replace non-existent super("feign") example
  with freemarker (real, verifiable); replace misleading single-name
  list with an accurate one; add sparkjava-2.3 counter-example
  explaining why super("sparkjava","sparkjava-2.4") uses the -2.4
  alias despite living in the -2.3/ directory (compile against 2.3,
  test against 2.4 for JettyHandler)
- muzzle.md: reframe testImplementation=min as default preference,
  not absolute rule; document justified deviation with sparkjava-2.3
  build.gradle as the canonical example
- context-tracking.md: reframe CompletableFuture pattern to emphasize
  that the correct (non-reassigning) pattern does NOT require
  @Advice.Return(readOnly=false); only add readOnly=false if you
  have a documented reason to substitute the return value
- advice-class.md: S1 framework-in-framework example now includes
  the null-check on activeSpan(), matching sparkjava's
  RoutesInstrumentation.java:52-55 (withRoute() does not guard)

Copilot's citations were checked against master before applying.
…tent

- Reword "regenerating an existing module" as "rewriting or refactoring" —
  the guidance applies whenever an existing module is being changed, not
  just to automated regeneration flows.
- Remove the "Rationale traceable to reviewer comments (PR/comment IDs)"
  footnote. That attribution belongs in commit messages and PR descriptions,
  not in the shipping skill; readers of the skill just need the rule.
@jordan-wong jordan-wong changed the title skill(apm-integrations): HTTP feedback cycle rules + super(...) reconciliation skill(apm-integrations): additional rules from recent HTTP-category PR reviews Jul 13, 2026
@jordan-wong jordan-wong marked this pull request as ready for review July 13, 2026 19:01
@jordan-wong jordan-wong requested a review from a team as a code owner July 13, 2026 19:01
@jordan-wong jordan-wong requested a review from mhlidd July 13, 2026 19:01
@jordan-wong jordan-wong self-assigned this Jul 13, 2026
@dd-octo-sts

dd-octo-sts Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@jordan-wong jordan-wong requested review from mcculls and removed request for mhlidd July 13, 2026 19:01

@datadog-prod-us1-3 datadog-prod-us1-3 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.

Datadog Autotest: PASS

More details

Documentation-only PR extending APM integration guidance with rules from recent reviewer feedback. All code examples verified against actual module implementations (sparkjava-2.3, jedis, okhttp, freemarker, liberty); naming reconciliation is empirically grounded and consistent; cross-references validated. No executable code changes — no regressions possible.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Datadog Autotest · Commit 04a0719 · What is Autotest? · Any feedback? Reach out in #autotest

@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: 04a07191ee

ℹ️ 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".

@Advice.OnMethodExit(suppress = Throwable.class)
public static void exit(@Advice.Return CompletableFuture<Response> future,
@Advice.Enter AgentSpan span) {
future.whenComplete((result, error) -> finishSpan(span, result, error));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid lambdas in the correct advice example

This “CORRECT” advice example still uses a lambda inside an @Advice.OnMethodExit, but the same skill’s advice guidance forbids lambdas in advice methods because they compile to synthetic call targets that are not helper-injected. When this pattern is copied into async client advice, ByteBuddy can inline a call site that references the advice class’s synthetic lambda method at runtime; existing callback advice uses declared callback/helper classes instead. Please show a named BiConsumer/helper callback while keeping the return read-only.

Useful? React with 👍 / 👎.

}
```

No `AgentScope`, no `startSpan()`, no `decorator.afterStart()`. This applies to any inner routing/dispatch framework that runs inside another instrumented HTTP server. It does NOT apply to standalone HTTP clients, which own their own span identity.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit route-only guidance to route enrichers

This blanket rule says it applies to any inner routing/dispatch framework and lists Ratpack/JAX-RS, but current instrumentations for those contexts intentionally create framework/controller spans in addition to enriching the outer span: JaxRsAnnotationsAdvice calls startSpan in dd-java-agent/instrumentation/rs/jax-rs/.../JaxRsAnnotationsInstrumentation.java:128, and Ratpack does the same in dd-java-agent/instrumentation/ratpack-1.5/.../TracingHandler.java:41. If the skill is followed for those frameworks, new server framework instrumentation would drop expected child spans rather than merely avoiding Spark-style span rewriting; please narrow this to route-only enrichers or carve out frameworks that own a handler span.

Useful? React with 👍 / 👎.

}

// CORRECT — attach the callback for its side-effect; keep the return read-only
@Advice.OnMethodExit(suppress = Throwable.class)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add onThrowable to the async exit example

The “CORRECT” async-client exit advice omits onThrowable = Throwable.class, so if a copied implementation starts a span on enter and the instrumented method throws before returning its future, ByteBuddy will skip this exit advice and the span/scope cleanup path will never run. This contradicts the advice lifecycle rule above in the same file; please include onThrowable = Throwable.class in the correct pattern and handle the thrown case separately from attaching the completion callback.

Useful? React with 👍 / 👎.


If the target method delegates to a sync client that is already instrumented (common in async-wrapper classes like `AsyncFeignClient`, `AsyncHttpClient`, etc.), do NOT open a second span in the async wrapper. The sync client's advice already opens the client span; wrapping again produces two spans per request, with the outer span holding no additional context.

Before adding advice to an async wrapper, trace the call path to the sync delegate. If the delegate is already instrumented for span emission, the async wrapper only needs context-propagation advice (capture on submission, restore on completion) — not a second span. See `context-tracking.md` for the propagation pattern.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore context before the sync delegate runs

For async wrappers that submit the sync client call to a worker thread, restoring context only on completion is too late: the sync client's advice creates the HTTP span while the worker executes, before the future completes, so it will not see the captured parent and can be emitted as a root or under the wrong context. The wrapper still should not create a second client span, but the propagation advice needs to reactivate around the delegate/executor task (or rely on executor instrumentation) and separately around user completion callbacks.

Useful? React with 👍 / 👎.

@jordan-wong

Copy link
Copy Markdown
Contributor Author

For reviewer reference, the two blind regenerations described in the behaviour-check comment above are now up as their own draft PRs (not for merging):

Each PR body includes a per-rule adherence table against the rules being proposed in this PR, showing which ones the current output honours vs violates. The sparkjava-2.3 output is the one to look at for the framework-inside-framework rule specifically — it violates that rule in a critical way, which is useful signal about whether the wording in this PR is strong enough.

@jordan-wong

Copy link
Copy Markdown
Contributor Author

Async cycle reference regens — done

Following up on the earlier behaviour-check retraction: the async-category blind regens are done, this time with the skill-loading path correctly configured. Two new draft reference PRs:

Both were generated against the current draft of this PR's skill (F2, S1, CI-1, master-override preservation, etc.), and this time the trace confirms the draft skill was actually loaded (unlike the retracted 2026-07-13 attempt).

The reactor run is notably smoother than rxjava3 (0 test-fix iterations, 1 reviewer iter, ~⅓ the cost) — could be Flux+Mono being simpler than rxjava's 6-type surface, or the skill guiding cleaner Cat B code. Reviewers may find that comparison useful signal.

I'll be monitoring CI on both and fixing what I can. Any failures that require skill changes will feed back into this PR.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants