Skip to content

Commit fc6f5e4

Browse files
jordan-wongmccullsdevflow.devflow-routing-intake
authored
skill(apm-integrations): add rules from recent reviews + refactor into sub references (#11760)
skill(add-apm-integrations): R13-R33 + Cat B (context-propagation) Step 4.4 Port reviewer-encoded rules from the toolkit's synced derivative (`apm-integrations` skill in DataDog/apm-instrumentation-toolkit) back to the canonical `add-apm-integrations` skill. The toolkit accumulated 21+ numbered reviewer rules during eval research (commits flowing through PR #11337 jedis-3.0, PR #11562 sparkjava-2.3, PR #11717 commons-httpclient, PR #11709 feign, and PR #11506 RxJava 3). These rules encode real failure modes observed during agent-generated PRs and the fixes reviewers asked for. Sections added: - **Step 4.4** — Library category: span-creating vs context-propagation. New axis that routes Cat B libraries (reactive, async, executors, futures, actors) into a `context_propagation` codegen path producing `InstrumenterModule.ContextTracking` instead of span-creating advice. Includes the Flowable subscribe(FlowableSubscriber) overload rule (hook the framework-internal overload, not the public wrapper). - **Step 4.5** — Java naming consistency (module-name conventions). R-rule placements: - Step 4: R32 (dir name must end with version OR allowed suffix) - Step 5: R13 (no single-type helper class for CallDepthThreadLocalMap) R30 (preserve master's integration name when regenerating) - Step 7: R15/R16/R17 (single delegate method, not all overloads) R33 (no NullPointerException catches; use null-check guards) - Step 9: R14 (test error/exception scenarios + spotless) R18 (muzzle excludes incompatible majors) R19 (latestDepTestImplementation range matches instrumented) R20 (Java tests only; no new .groovy files) R28 (compileOnly/testImplementation version split rationale) R29 (register names in supported-configurations.json) R31 (assertInverse only when declared min is true min) - Step 4.4: R21-R27 (the Cat A vs Cat B classification + Cat B schema) This PR pairs with toolkit-side PR DataDog/apm-instrumentation-toolkit#472 which adds the same content to the toolkit's synced derivative copy. Both copies should stay in sync — this is the canonical home. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> skill(add-apm-integrations): clean up R-numbering and sub-step naming Two cosmetic passes on the rule encoding ported from the toolkit's synced derivative copy. Substantive content unchanged. Pass 1 — strip R-numbering. The R13-R33 numbers are toolkit-internal traceability tags that tie each rule to a specific reviewer comment on a specific generated-PR review. They are meaningful to the toolkit-side eval research workflow but meaningless to readers of the canonical skill, who have no R1-R12 context here. Removes: - '#### R<NN> — <title>' prefixes (14 sub-section headings) - Inline '(R<NN>)', '(R<NN>/R<NN>/R<NN>)' parentheticals on existing bullets that already convey the rule in their wording - A stale cross-reference 'see R29 below' rewritten to 'see "Register new integration names"' The toolkit-side copy keeps the R-numbering — it remains the eval-research home where the traceability matters. This is a one-way port convention. Pass 2 — renumber half-steps so the decimals make sense. Was: Step 4 → Step 4.4 → Step 4.5 → Step 5 implying missing 4.1, 4.2, 4.3. The original numbering was an artifact of an earlier toolkit-side draft that had a 4.1-4.4 enumeration which got collapsed. Renumbered to: Step 4 → Step 4.1 → Step 4.2 → Step 5 Main integer steps (1-12) unchanged. Step 7.1 (Multiple advice classes and @AppliesOn) left as-is since its decimal already makes sense relative to Step 7. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): extract Category B routing to references/ Move the 62-line "Step 4.1 – Library category" section from SKILL.md into references/category-b-context-propagation.md. SKILL.md keeps a 5-line stub linking to the reference file — enough context to know when to read it, not enough to bury the rest of Step 4. Preserves all content verbatim; no wording changes. Follows the existing dd-trace-java skill convention of tracking specific per-skill files (git add -f, matching the precedent set by .claude/skills/migrate-groovy-to-java/QUALITY_RULES.md). Part 1 of a refactor to slim the 794-line SKILL.md into a routing overview (~250 lines target) with topic-oriented reference files. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): extract naming conventions to references/ Move two related naming rules from SKILL.md into references/naming-conventions.md: - Step 4's module-directory-name rule (must end with version or "-common"/"-stubs"/"-iast" suffix) - Step 4.2's Java filename ↔ class-name matching rule (with the sanity-check script) They belong together because both are enforcement rules for names. SKILL.md keeps short stubs linking to the reference file. Preserves all content verbatim. Part 2 of the SKILL.md slim. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): extract InstrumenterModule guidance to references/ Move Step 5's 103-line body from SKILL.md into references/instrumenter-module.md, covering: - @autoservice + narrow-interface preferences (ForSingleType > ForKnownTypes > ForTypeHierarchy) with the interface-only API JAR exception (JMS, JPA, JDBC, etc.) - 'Must NOT do' — no static constants for one-shot methods - instrumentationNames() version-qualified alias rule - No helper class for single-target CallDepthThreadLocalMap - Preserve master's integration name on regeneration - Advanced: grouping multiple instrumentations under one module SKILL.md keeps Step 5 as a 6-line summary + link. Preserves all content verbatim; no wording changes. Part 3 of the SKILL.md slim. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): extract Advice class guidance to references/ Move Step 7's 149-line body from SKILL.md into references/advice-class.md. This was the largest single-step body in the skill and the highest-risk area to get wrong. Reference file covers: - Advice method annotations + parameter kinds - Span lifecycle (enter/exit order) - onExit resilience to onEnter throwing - Explicit charset for byte[] to String - No NullPointerException catches (SpotBugs enforces) - Single-delegate-method instrumentation (not all overloads) - @AppliesOn + multiple advice classes - 'Must NOT do' list (no loggers, no lambdas, no inline=false, etc.) SKILL.md keeps Step 7 as a summary + link. Preserves all content verbatim. Part 4 of the SKILL.md slim. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): extract tests guidance to references/ Move Step 9's 'Instrumentation test' section + all its sub-rules (no .groovy files, supported-configurations.json registration, compileOnly/testImplementation version-split rationale, prior-version-module inclusion) from SKILL.md into references/tests.md. Muzzle content stays in place for now — it's a separate concern and gets its own reference file next. SKILL.md keeps Step 9.1 as a summary + link. Preserves all content verbatim. Part 5 of the SKILL.md slim. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): extract muzzle guidance to references/ Move Step 9's 'Muzzle directives' section + all its sub-rules (assertInverse gotchas, incompatible-major-version exclusion, skipVersions for malformed release versions) from SKILL.md into references/muzzle.md. SKILL.md keeps Step 9.2 as a summary + link. Preserves all content verbatim; no wording changes. Part 6 of the SKILL.md slim. Final state: SKILL.md 794 → ~215 lines, split into 6 topic-oriented reference files under references/. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> style(skill): add missing blank lines before Step/subsection headings Cosmetic fixup after the section extractions. Three headings lost their preceding blank line during the awk-based edits — restoring them so the rendered Markdown reads cleanly. No content changes. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): drop toolkit-internal 'Category A/B' language The 'Category A' / 'Category B' labels came from toolkit-side research where they were shorthand for the 'target_kind' Pydantic enum values. They have no meaning in dd-trace-java on their own — a contributor reading the skill has no context for what 'Category B' refers to. Replace with the descriptive terms that already exist in dd-trace-java: - 'span-creating instrumentation' — extends InstrumenterModule.Tracing - 'context-tracking instrumentation' — extends InstrumenterModule.ContextTracking (matches the class name + TargetSystem.CONTEXT_TRACKING enum) Changes: - Rename references/category-b-context-propagation.md → references/context-tracking.md - Rewrite Step 4.1 stub in SKILL.md to drop Category A/B and 'target_kind' - Rewrite context-tracking.md body from 'Category B target shape' Pydantic- field enumeration to 'What a context-tracking instrumentation captures', described in Java terms (boundary type, capture/restore points, wrapper class, wrapper methods) instead of toolkit Pydantic field names - Fix advice-class.md's stray 'context-propagation logic' → 'context-tracking logic' to match dd-trace-java's TargetSystem.CONTEXT_TRACKING naming No substantive guidance changed. Reference still points at rxjava-2.0 as the canonical example. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> refactor(skill): remove toolkit-workflow language from reference files Two remaining spots reframed from LLM-agent-workflow perspective to dd-trace-java human-contributor perspective: - muzzle.md 'Background' paragraph: 'a typical greenfield generation produces...' + 'the agent picks the higher version...' → 'this failure mode is common when a module has both a sync and async instrumentation class' + 'declaring the higher version as the muzzle min...'. Same technical content, no LLM-agent workflow assumption. - tests.md 'How to discover' step: 'run the sample app' → 'run your instrumentation test'. 'Sample app' was ambiguous ('the toolkit's sample-app workflow step' vs 'your own test app'); the concrete dd-trace-java term is 'instrumentation test'. No substantive guidance changed. Preserves all rules verbatim. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> fix(skill): address review comments on #11760 Nine fixes from Copilot bot + @mcculls review comments: SKILL.md - Step 4 source layout: 'src/test/groovy/ — Spock tests' → 'src/test/java/ — JUnit 5 tests'. Contradicted Step 9.1's Java-only policy. (Copilot) references/tests.md - Rewrite the error-test example: 'List<List<SpanData>> traces = ...' used OpenTelemetry's SpanData type (won't compile against dd-trace-java's TEST_WRITER, which returns List<List<DDSpan>>). Now uses AgentSpan and span.getTag() per mcculls's guidance that AgentSpan is enough for tests. - Replace 'checkNewGroovyFiles' (unverifiable bot name) with the real workflow: 'Enforce Groovy Migration' (.github/workflows/enforce-groovy-migration.yaml). Both places. - Default value in supported-configurations.json: change 'false' to 'true' per mcculls — ~83% of typical integrations default to true; 'false' is reserved for modules that override defaultEnabled() (OpenTelemetry, Hazelcast, sparkjava). Add a note calling out the branching. references/naming-conventions.md - Remove gRPCInstrumentation as an example — it doesn't exist in the codebase; the gRPC integration uses Grpc* (GrpcClientDecorator etc). Reframe the section to acknowledge acronym casing is not uniform across dd-trace-java and to defer to a reference instrumentation. (Copilot) - Drop the sanity-check bash script entirely. mcculls flagged that its regex only matched 'class', missing enum/interface/@interface, and would produce false MISMATCH lines for any such file (LogHandler.java, ParameterCollector.java, etc.). references/advice-class.md - Rewrite the 'onExit resilient to onEnter throwing' section — the claim that 'onThrowable = Throwable.class ensures exit fires even on onEnter exception' was factually wrong. Per docs/how_instrumentations_work.md:532-552, 'if the OnMethodEnter method throws an exception, the OnMethodExit method is not invoked' — unconditionally; onThrowable cannot override it. onThrowable controls exit-on-target-method-throw, not exit-on-enter-throw. (mcculls) - Add inline note that java.nio.charset.StandardCharsets is a java.nio.* type and forbidden in bootstrap instrumentations (per the same file's Must NOT list). In bootstrap advice, use the string charset name ('UTF-8') instead. (Copilot) references/context-tracking.md - Soften the 'rxjava-2.0 hooks subscribe(Observer)' statement. The module's actual matcher is named('subscribe').and(takesArguments(1)), matching any single-arg subscribe overload with the argument typed as the base callback interface. Direct the reader at the module source instead of copying overload names. (Copilot) Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> fix(skill): trim bootstrap note on charset to a single line Copilot's suggestion was 'add an explicit note here'; the initial fix was a full paragraph. Trimming to a single-sentence pointer since the Must NOT list already carries the details. Signed-off-by: Jordan Wong <jordan.wong@datadoghq.com> review feedback Fix docs inconsistencies Rename skill to apm-integrations remove redundant note Renamed .claude/skills to .agents/skills and added symlink-style redirects for the old location Align NIO docs advice with skill Simplify SKILL Fix constructor advice best-practice Address codex comments Co-authored-by: mcculls <stuart.mcculloch@datadoghq.com> Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 731a621 commit fc6f5e4

23 files changed

Lines changed: 801 additions & 294 deletions
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
name: apm-integrations
3+
description: Write a new library instrumentation end-to-end. Use when the user ask to add a new APM integration or a library instrumentation.
4+
context: fork
5+
allowed-tools:
6+
- Bash
7+
- Read
8+
- Write
9+
- Edit
10+
- Glob
11+
- Grep
12+
---
13+
14+
Write a new APM end-to-end integration for dd-trace-java, based on library instrumentations, following all project conventions.
15+
16+
## Step 1 – Read the authoritative docs and sync this skill (mandatory, always first)
17+
18+
Before writing any code, read all three files in full:
19+
20+
1. [`docs/how_instrumentations_work.md`](docs/how_instrumentations_work.md) — full reference (types, methods, advice, helpers, context stores, decorators)
21+
2. [`docs/add_new_instrumentation.md`](docs/add_new_instrumentation.md) — step-by-step walkthrough
22+
3. [`docs/how_to_test.md`](docs/how_to_test.md) — test types and how to run them
23+
24+
These files are the single source of truth. Reference them while implementing.
25+
26+
## Step 2 – Clarify the task
27+
28+
If the user has not already provided all of the following, ask before proceeding:
29+
30+
- **Framework name** and **minimum supported version** (e.g. `okhttp-3.0`)
31+
- **Target class(es) and method(s)** to instrument (fully qualified class names preferred)
32+
- **Target system**: one of `Tracing`, `Profiling`, `AppSec`, `Iast`, `CiVisibility`, `Usm`, `ContextTracking`
33+
- **Whether this is a bootstrap instrumentation** (affects allowed imports)
34+
35+
## Step 3 – Find a reference instrumentation
36+
37+
Search `dd-java-agent/instrumentation/` for a structurally similar integration:
38+
- Same target system
39+
- Comparable type-matching strategy (single type, hierarchy, known types)
40+
41+
Read the reference integration's `InstrumenterModule`, Advice, Decorator, and test files to understand the established
42+
pattern before writing new code. Use it as a template.
43+
44+
## Step 4 – Set up the module
45+
46+
1. Create directory: `dd-java-agent/instrumentation/$framework/$framework-$minVersion/`
47+
2. Under it, create the standard Maven source layout:
48+
- `src/main/java/` — instrumentation code
49+
- `src/test/groovy/` — Groovy/Spock instrumentation tests (see Step 9.1)
50+
3. Create `build.gradle` with:
51+
- `compileOnly` dependencies for the target framework
52+
- `testImplementation` dependencies for tests
53+
- `muzzle { pass { } }` directives (see Step 9.2)
54+
4. Register the new module in `settings.gradle.kts` in **alphabetical order**
55+
5. Register all integration names in `metadata/supported-configurations.json`**read [Supported Configurations](references/supported-configurations.md)** for the exact key shapes and CI checks involved. Declaring several names (`super("a", "b")`) means one entry each.
56+
57+
**See [Naming Conventions](references/naming-conventions.md) — module directory name must end with a version or an allowed suffix (`-common`, `-stubs`, `-iast`). Java filename and `public class` name MUST match character-for-character including acronym casing (CRITICAL — see § "Java naming consistency").**
58+
59+
## Step 4.1 – Span-creating vs context-tracking instrumentation
60+
61+
Read [Context-Tracking Instrumentation](references/context-tracking.md) and decide whether the library needs `InstrumenterModule.Tracing` (I/O operations that create spans) or `InstrumenterModule.ContextTracking` (async-boundary bridging, no spans).
62+
63+
## Step 5 – Write the InstrumenterModule
64+
65+
**Read [InstrumenterModule Guidance](references/instrumenter-module.md).**
66+
67+
## Step 6 – Write the Decorator
68+
69+
- Extend the most specific available base decorator:
70+
- `HttpClientDecorator`, `DatabaseClientDecorator`, `ServerDecorator`, `MessagingClientDecorator`, etc.
71+
- One `public static final DECORATE` instance
72+
- Define `UTF8BytesString` constants for the component name and operation name
73+
- Keep all tag/naming/error logic here — not in the Advice class
74+
- Override `spanType()`, `component()`, `spanKind()` as appropriate
75+
- Override `instrumentationNames()` to return the primary integration name without a version suffix: `return new String[] {"jedis"};` not `"jedis-3.0"`.
76+
- `BaseDecorator` uses those names to resolve analytics settings (`DD_TRACE_<NAME>_ANALYTICS_ENABLED`, `DD_TRACE_<NAME>_ANALYTICS_SAMPLE_RATE`). Search `metadata/supported-configurations.json` for each returned name — if analytics keys are absent, add them (see [Supported Configurations](references/supported-configurations.md) for the JSON shape).
77+
78+
## Step 7 – Write the Advice class (highest-risk step)
79+
80+
**Read [Writing the Advice Class](references/advice-class.md) — the highest-risk step.** Pay particular attention to: `@Advice.OnMethodEnter/Exit` annotations; `CallDepthThreadLocalMap` reentrancy guarding; span lifecycle order; and the "Must NOT do" list.
81+
82+
## Step 8 – Add SETTER/GETTER adapters (if applicable)
83+
84+
For context propagation to and from upstream services, like HTTP headers,
85+
implement `AgentPropagation.Setter` / `AgentPropagation.Getter` adapters that wrap the framework's specific header API.
86+
Place them in the helpers package, declare them in `helperClassNames()`.
87+
88+
## Step 9 – Write tests
89+
90+
Cover all mandatory test types:
91+
92+
### 1. Instrumentation test (mandatory)
93+
94+
**Read [Writing Tests](references/tests.md).** Instrumentation tests are Groovy/Spock (`src/test/groovy/`) — add the `tag: override groovy enforcement` label to suppress the `Enforce Groovy Migration` CI check (which blocks new `.groovy` files by default — instrumentation tests are intentionally Groovy/Spock). Must cover error/exception scenarios. When adding new integration names, register them per [Supported Configurations](references/supported-configurations.md). When `compileOnly` and `testImplementation` use different versions, comment the specific class that requires the higher version. Include sibling version modules as `testImplementation` dependencies for mutual-exclusion tests.
95+
96+
### 2. Muzzle directives (mandatory)
97+
98+
**Read [Muzzle Directives](references/muzzle.md)** — it covers all three valid patterns and their `assertInverse` rules. Search adjacent module `build.gradle` files for `skipVersions` before declaring a new version-bounded module's muzzle directives.
99+
100+
### 3. Latest dependency test (mandatory)
101+
102+
Use `latestDepTestImplementation` in `build.gradle` to pin the latest available version. Run with:
103+
```bash
104+
./gradlew :dd-java-agent:instrumentation:$framework:$framework-$version:latestDepTest
105+
```
106+
107+
**`latestDepTestImplementation` version range must match the instrumented range.** If your module instruments version `2.x`, use `2.+` as the version constraint, not `3.+`:
108+
109+
```groovy
110+
// WRONG — latestDep tests against 3.x but the module only instruments 2.x
111+
latestDepTestImplementation group: 'commons-httpclient', name: 'commons-httpclient', version: '3.+'
112+
113+
// CORRECT — latestDep tests against the highest 2.x release
114+
latestDepTestImplementation group: 'commons-httpclient', name: 'commons-httpclient', version: '2.+'
115+
```
116+
117+
Using `3.+` for a `2.x` instrumentation means `latestDepTest` runs against an incompatible API version and will either fail or silently test nothing.
118+
119+
### 4. Smoke test (optional)
120+
121+
Add a smoke test in `dd-smoke-tests/` only if the framework warrants a full end-to-end demo-app test.
122+
123+
## Step 10 – Build and verify
124+
125+
Run these commands in order and fix any failures before proceeding:
126+
127+
```bash
128+
./gradlew :dd-java-agent:instrumentation:$framework:$framework-$version:muzzle
129+
./gradlew :dd-java-agent:instrumentation:$framework:$framework-$version:test
130+
./gradlew :dd-java-agent:instrumentation:$framework:$framework-$version:latestDepTest
131+
./gradlew checkInstrumenterModuleConfigurations
132+
./gradlew checkDecoratorAnalyticsConfigurations
133+
./gradlew spotlessApply
134+
./gradlew :dd-java-agent:updateAgentJarIntegrationsGoldenFile
135+
```
136+
137+
After `updateAgentJarIntegrationsGoldenFile` runs, commit the updated `metadata/agent-jar-checks.properties` file alongside your instrumentation changes. The `verifyAgentJarIntegrations` check runs automatically in CI and fails if this file is out of date.
138+
139+
**If muzzle fails:**
140+
- Missing helper class names in `helperClassNames()` — the most common cause; add any missing inner, anonymous, or enum synthetic classes.
141+
- Wrong version range — the declared `versions` in `build.gradle` doesn't cover the versions actually used by tests; adjust the bounds.
142+
- API mismatch — the instrumented class or method doesn't exist in the declared version; check the library's changelog and narrow the `compileOnly` version or the muzzle range.
143+
144+
**If `checkInstrumenterModuleConfigurations` fails:** an integration name from `super(...)` is missing
145+
(or mismatched) in `metadata/supported-configurations.json` — see [Supported Configurations](references/supported-configurations.md).
146+
147+
**If `checkDecoratorAnalyticsConfigurations` fails:** a name returned by the decorator's `instrumentationNames()` is missing `DD_TRACE_<NAME>_ANALYTICS_ENABLED` / `DD_TRACE_<NAME>_ANALYTICS_SAMPLE_RATE` entries in `metadata/supported-configurations.json` — add them per [Supported Configurations](references/supported-configurations.md).
148+
149+
**If tests fail:** verify span lifecycle order (start → activate → error → close → finish), helper registration,
150+
and `contextStore()` map entries match actual usage.
151+
152+
## Step 11 – Checklist before finishing
153+
154+
Output this checklist and confirm each item is satisfied:
155+
156+
- [ ] `settings.gradle.kts` entry added in alphabetical order
157+
- [ ] `metadata/supported-configurations.json` has a `DD_TRACE_<NAME>_ENABLED` entry (+ the two aliases) for every name passed to `super(...)`
158+
- [ ] `metadata/supported-configurations.json` has `DD_TRACE_<NAME>_ANALYTICS_ENABLED` and `DD_TRACE_<NAME>_ANALYTICS_SAMPLE_RATE` entries for every name returned by the decorator's `instrumentationNames()`
159+
- [ ] `build.gradle` has `compileOnly` deps and `muzzle` directives
160+
- [ ] Muzzle pattern is correct (see [Muzzle Directives](references/muzzle.md))
161+
- [ ] `latestDepTestImplementation` version range matches the instrumented version range (e.g. `2+` not `3+` for a `2.x` module)
162+
- [ ] `@AutoService(InstrumenterModule.class)` annotation present on the module class
163+
- [ ] `helperClassNames()` lists ALL referenced helpers (including inner, anonymous, and enum synthetic classes)
164+
- [ ] Advice methods are `static` with `@Advice.OnMethodEnter` / `@Advice.OnMethodExit` annotations
165+
- [ ] `@Advice.OnMethodEnter(suppress = Throwable.class)` on enter; `@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)` on exit (omit `onThrowable` when hooking a constructor)
166+
- [ ] No static constants holding return values of one-shot instrumenter methods (`triggerClasses()`, `contextStore()`, etc.)
167+
- [ ] No logger field in the Advice class or InstrumenterModule class
168+
- [ ] No `inline=false` left in production code
169+
- [ ] No `java.util.logging.*` / `java.nio.file.*` / `javax.management.*` in bootstrap path
170+
- [ ] `metadata/agent-jar-checks.properties` updated via `./gradlew :dd-java-agent:updateAgentJarIntegrationsGoldenFile` and committed
171+
- [ ] Span lifecycle order is correct: startSpan → afterStart → activateSpan (enter); onError → beforeFinish → close → finish (exit)
172+
- [ ] All Step 10 verification commands passed with no failures
173+
174+
## Step 12 – Retrospective: update this skill with what was learned
175+
176+
After the instrumentation is complete (or abandoned), review the full session and improve this skill for future use.
177+
178+
**Collect lessons from four sources:**
179+
180+
1. **Build/test failures** — did any Gradle task fail with an error that this skill did not anticipate or gave wrong
181+
guidance for? (e.g. a muzzle failure that wasn't caused by missing helpers, a test pattern that didn't work)
182+
2. **Docs vs. skill gaps** — did Step 1's sync miss anything? Did you consult the docs for something not captured here?
183+
3. **Reference instrumentation insights** — did the reference integration use a pattern, API, or convention not
184+
reflected in any step of this skill?
185+
4. **User corrections** — did the user correct an output, override a decision, or point out a mistake?
186+
187+
**For each lesson identified**, edit this file (`.agents/skills/apm-integrations/SKILL.md`) or its referenced files
188+
using the `Edit` tool:
189+
- Wrong rule → fix it in place
190+
- Missing rule → add it to the most relevant step
191+
- Wrong failure guidance → update the relevant "If X fails" section in Step 10
192+
- Misleading or obsolete content → remove it
193+
194+
Keep each change minimal and targeted. Do not rewrite sections that worked correctly.
195+
After editing, confirm to the user which improvements were made to the skill.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Writing the Advice Class
2+
3+
> Referenced from `SKILL.md` Step 7. The highest-risk step — every rule in this file exists because someone's PR broke on it.
4+
5+
## Must do
6+
7+
- Advice methods **must** be `static`
8+
- Annotate enter: `@Advice.OnMethodEnter(suppress = Throwable.class)`
9+
- Annotate exit: `@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)`
10+
- **Exception**: do NOT use `onThrowable` when hooking a constructor — if the constructor throws, `@Advice.This` is a partially initialized object
11+
- Use `@Advice.Local("...")` for values shared between enter and exit (span, scope)
12+
- Use the correct parameter annotations:
13+
- `@Advice.This` — the receiver object
14+
- `@Advice.Argument(N)` — a method argument by index
15+
- `@Advice.Return` — the return value (exit only)
16+
- `@Advice.Thrown` — the thrown exception (exit only)
17+
- `@Advice.Enter` — the return value of the enter method (exit only)
18+
- Use `CallDepthThreadLocalMap` to guard against recursive instrumentation of the same method
19+
- **Instrument the single delegate method, not all overloads**: when a library has multiple overloads of the same operation (e.g. `executeMethod(String)`, `executeMethod(HostConfig)`, `executeMethod(HostConfig, HttpMethod)`), check if they all delegate to a single internal method. If yes, instrument ONLY the delegate — not each overload. Instrumenting all overloads without a proper reentrancy guard creates **duplicate spans per request** (one per overload in the call chain) and injects context propagation headers multiple times. Use `CallDepthThreadLocalMap` when you must instrument at a higher level.
20+
21+
## Span lifecycle (in order)
22+
23+
Enter method:
24+
1. `AgentSpan span = startSpan(DECORATE.operationName(), ...)`
25+
2. `DECORATE.afterStart(span)` + set domain-specific tags
26+
3. `AgentScope scope = activateSpan(span)` — return or store via `@Advice.Local`
27+
28+
Exit method:
29+
4. `DECORATE.onError(span, throwable)` — only if throwable is non-null
30+
5. `DECORATE.beforeFinish(span)`
31+
6. `scope.close()`
32+
7. `span.finish()`
33+
34+
### onExit handling when the target method throws
35+
36+
The `onThrowable = Throwable.class` attribute on `@Advice.OnMethodExit` controls whether the exit advice fires when the **instrumented target method** throws. You **must** set it explicitly to `Throwable.class` for any exit advice that closes a scope or finishes a span — the default skips exceptional termination, which leaks active scopes when the instrumented method throws.
37+
38+
```java
39+
// Standard pattern — exit fires whether the target method returned or threw
40+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
41+
public static void exit(
42+
@Advice.Enter final AgentScope scope,
43+
@Advice.Thrown final Throwable thrown) {
44+
if (scope != null) {
45+
AgentSpan span = scope.span();
46+
DECORATE.onError(span, thrown);
47+
DECORATE.beforeFinish(span);
48+
scope.close();
49+
span.finish();
50+
}
51+
}
52+
```
53+
54+
**`onThrowable` does NOT compensate for `onEnter` throwing.** Per `docs/how_instrumentations_work.md`: "If the `Advice.OnMethodEnter` method throws an exception, the `Advice.OnMethodExit` method is not invoked" — this is unconditional. To keep `onEnter` from throwing in the first place, use `suppress = Throwable.class` on the enter advice.
55+
56+
When using `CallDepthThreadLocalMap`, only the outermost call (the one where `incrementCallDepth` returned 0) should reset the counter. Recursive inner calls that returned early on enter must also return early on exit without resetting — otherwise an inner exit clears the counter while the outer call is still active, allowing subsequent nested calls to create duplicate spans. The exit guard must mirror the enter guard exactly.
57+
58+
### Specify charset explicitly when converting byte[] to String
59+
60+
```java
61+
// WRONG — uses platform default charset
62+
String cmd = new String(commandBytes);
63+
64+
// CORRECT — explicit charset
65+
import java.nio.charset.StandardCharsets;
66+
String cmd = new String(commandBytes, StandardCharsets.UTF_8);
67+
```
68+
69+
### Do NOT catch `NullPointerException`; use null-check guards instead
70+
71+
Catching `NullPointerException` is always a sign of an unguarded precondition — fix the root cause with an explicit null check instead. dd-trace-java enforces this via SpotBugs rule `DCN_NULLPOINTER_EXCEPTION`; violations fail `:spotbugsMain` and block the PR.
72+
73+
```java
74+
// WRONG — SpotBugs DCN_NULLPOINTER_EXCEPTION
75+
@Override
76+
protected int status(final HttpMethod httpMethod) {
77+
try {
78+
return httpMethod.getStatusCode();
79+
} catch (NullPointerException e) {
80+
// getStatusCode() throws NPE when statusLine is null
81+
return 0;
82+
}
83+
}
84+
85+
// CORRECT — null-check guard (this is the canonical master pattern)
86+
@Override
87+
protected int status(final HttpMethod httpMethod) {
88+
final StatusLine statusLine = httpMethod.getStatusLine();
89+
return statusLine == null ? 0 : statusLine.getStatusCode();
90+
}
91+
```
92+
93+
**How to discover**: when implementing a method that calls library code which may NPE on null internal state, READ the master module's analogous method for the canonical null-check pattern. The master typically exposes the nullable intermediate (e.g. `getStatusLine()`) so you can guard it.
94+
95+
## Multiple advice classes and `@AppliesOn`
96+
97+
If your instrumentation needs to apply multiple advices to the same method (e.g. separate context-tracking from tracing logic), use `applyAdvices()` inside `methodAdvice()`. Use the `@AppliesOn` annotation to control which target systems each advice applies to.
98+
99+
See the `@AppliesOn Annotation` section of `docs/how_instrumentations_work.md` for the full API and examples.
100+
101+
## Must NOT do
102+
103+
- **No logger fields** in the Advice class or the Instrumentation class (loggers only in helpers/decorators)
104+
- **No code in the Advice constructor** — it is never called
105+
- **Do not use lambdas in advice methods** — they create synthetic classes that will be missing from helper declarations
106+
- **No references** to other methods in the same Advice class or in the InstrumenterModule class
107+
- **No `InstrumentationContext.get()`** outside of Advice code
108+
- **No `inline=false`** in production code (only for debugging; must be removed before committing)
109+
- **No `java.util.logging.*`, `java.nio.file.*`, or `javax.management.*`** in bootstrap instrumentations

0 commit comments

Comments
 (0)