Skip to content

claude_code: add Claude Code OTel integration#19765

Open
efd6 wants to merge 2 commits into
elastic:mainfrom
efd6:19407-claude_code
Open

claude_code: add Claude Code OTel integration#19765
efd6 wants to merge 2 commits into
elastic:mainfrom
efd6:19407-claude_code

Conversation

@efd6

@efd6 efd6 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Proposed commit message

claude_code: add Claude Code OTel integration

Collect telemetry from Claude Code CLI sessions via OpenTelemetry.
The package includes an ingest pipeline that flattens OTel attributes
to root-level fields, enriches ECS fields (event.action, event.outcome,
related.user, related.hosts), and six Kibana dashboards covering
session timeline, security overview, cost/usage, MCP server access,
tool calls, and permission decisions.

Test samples were collected from live sessions and sanitised.

Note

Apologies for the size. The OTel system tests need a mock server and the inputs to all pipeline tests are JSON, rather than lines.

Also, ref elastic/elastic-package#3711.

Checklist

  • I have reviewed tips for building integrations and this pull request is aligned with them.
  • I have verified that all data streams collect metrics or logs.
  • I have added an entry to my package's changelog.yml file.
  • I have verified that Kibana version constraints are current according to guidelines.
  • I have verified that any added dashboard complies with Kibana's Dashboard good practices

Author's Checklist

  • [ ]

How to test this PR locally

Related issues

Screenshots

Screenshot from 2026-06-26 13-36-17

@efd6 efd6 requested a review from a team June 26, 2026 04:26
@efd6 efd6 self-assigned this Jun 26, 2026
@efd6 efd6 added enhancement New feature or request Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations] Integration:claude_code [Integration not found in source] labels Jun 26, 2026
@efd6 efd6 force-pushed the 19407-claude_code branch from 03363c0 to f133400 Compare June 26, 2026 04:26
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Elastic Docs Style Checker (Vale)

Summary: 3 suggestions found

💡 Suggestions (3): Optional style improvements. Apply when helpful.
File Line Rule Message
packages/claude_code/_dev/build/docs/README.md 57 Elastic.WordChoice Consider using 'deactivated, deselected, hidden, turned off, unavailable' instead of 'disabled', unless the term is in the UI.
packages/claude_code/_dev/build/docs/README.md 61 Elastic.WordChoice Consider using 'deactivated, deselected, hidden, turned off, unavailable' instead of 'disabled', unless the term is in the UI.
packages/claude_code/_dev/build/docs/README.md 108 Elastic.WordChoice Consider using 'can, might' instead of 'may', unless the term is in the UI.

The Vale linter checks documentation changes against the Elastic Docs style guide. To use Vale locally or report issues, refer to Elastic style guide for Vale.

@elastic-vault-github-plugin-prod

Copy link
Copy Markdown

🚀 Benchmarks report

To see the full report comment with /test benchmark fullreport

@efd6 efd6 marked this pull request as ready for review June 26, 2026 04:52
@efd6 efd6 requested a review from a team as a code owner June 26, 2026 04:52
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

Pinging @elastic/security-service-integrations (Team:Security-Service Integrations)

@vera-review-bot

Copy link
Copy Markdown

👀 I have started reviewing the PR

if: ctx.error_detail != null

- append:
tag: append_related_user_email

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM data_stream/events/.../default.yml:260

related.user enrichment never fires (nested access on flat key)

The append_related_user_email and append_related_user_id processors guard on ctx.user?.email and ctx.user?.id (nested map access) and template the value as {{{user.email}}} / {{{user.id}}}. But the earlier flatten_attributes_to_root script copies OTel attributes to the root with ctx[key] = val, so user.email and user.id arrive as literal flat dotted keys (ctx['user.email']), not a nested user object. ctx.user is therefore always null, both if conditions are always false, and the processors never run. This is confirmed by every expected fixture that carries user data (e.g. test-tool-result-bash.json-expected.json, test-api-request.json-expected.json, test-plugin-loaded.json-expected.json): each has flat user.email/user.id but no related.user field at all. By contrast append_related_hosts works because it reads the single-token key ctx.server_name (test-mcp-server-connection.json-expected.json shows related.hosts populated). For a security-focused integration, related.user is the user-correlation pivot used by the dashboards, so it is silently empty on every event.

Recommendation:

Read the flat dotted keys explicitly instead of nested access. A script processor handles the array append with de-duplication cleanly:

- script:
    tag: set_related_user
    lang: painless
    description: Populate related.user from the flattened user identifiers.
    source: >
      def vals = new ArrayList();
      def email = ctx['user.email'];
      def id = ctx['user.id'];
      if (email != null) { vals.add(email); }
      if (id != null && !vals.contains(id)) { vals.add(id); }
      if (!vals.isEmpty()) {
        ctx.related = ctx.related ?: new HashMap();
        ctx.related.user = vals;
      }

Alternatively, have flatten_attributes_to_root reconstruct a nested user map (as it already does for event) so the existing append processors' ctx.user?.email checks resolve.


🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills

⚠️ Automated review — verify suggestions before applying.

@efd6 efd6 force-pushed the 19407-claude_code branch from f133400 to f3cdc5c Compare June 26, 2026 06:33
@vera-review-bot

Copy link
Copy Markdown

👀 I have started reviewing the PR

@vera-review-bot

Copy link
Copy Markdown

Vera Review Bot

For the current commit state, I did not find any issues.


🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills

⚠️ Automated review — verify suggestions before applying.

Collect telemetry from Claude Code CLI sessions via OpenTelemetry.
The package includes an ingest pipeline that flattens OTel attributes
to root-level fields, enriches ECS fields (event.action, event.outcome,
related.user, related.hosts), and six Kibana dashboards covering
session timeline, security overview, cost/usage, MCP server access,
tool calls, and permission decisions.

Test samples were collected from live sessions and sanitised.
@efd6 efd6 force-pushed the 19407-claude_code branch from f3cdc5c to 12be857 Compare June 26, 2026 06:48
@vera-review-bot

Copy link
Copy Markdown

👀 I have started reviewing the PR

@vera-review-bot

Copy link
Copy Markdown

Vera Review Bot

For the current commit state, I did not find any issues.


🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills

⚠️ Automated review — verify suggestions before applying.

@github-actions

Copy link
Copy Markdown
Contributor

TL;DR

Build #45096 failed because packages/claude_code/docs/README.md was stale: the committed generated field table still used e.g. text while the package source regenerated it as for example. The current PR head (12be8576faae85f4cdbe06eeb7f7a20e9aeca939) already contains the regenerated README, and Buildkite build #45097 is pending on that newer commit.

Remediation

  • No additional code change is needed for the latest PR head if build #45097 passes.
  • If this reappears, regenerate the package docs from the field definitions so packages/claude_code/docs/README.md matches packages/claude_code/data_stream/events/fields/fields.yml, then rerun .buildkite/scripts/test_one_package.sh packages/claude_code origin/main <latest-sha>.
Investigation details

Root Cause

The failed build ran on commit f3cdc5cade07c5d3de0121175d8d8aba3d806a1e. At that commit, packages/claude_code/data_stream/events/fields/fields.yml had field descriptions such as for example user-install, but the committed rendered docs still had e.g. in packages/claude_code/docs/README.md (for example lines 271, 291, 305, 342, and 348). The package check regenerated the docs and failed because the rendered output differed from the checked-in README.

The current head 12be8576faae85f4cdbe06eeb7f7a20e9aeca939 has the regenerated values in packages/claude_code/docs/README.md (for example enabled_via at line 277 and tool_name at line 348), matching the source field descriptions.

Evidence

-| enabled_via | How the plugin was enabled (e.g. user-install). | keyword |
+| enabled_via | How the plugin was enabled (for example user-install). | keyword |
...
-| tool_name | Name of the tool invoked (e.g. Bash, Read, Write, mcp_tool). | keyword |
+| tool_name | Name of the tool invoked (for example Bash, Read, Write, mcp_tool). | keyword |
Error: checking package failed: checking readme files are up-to-date failed: files do not match

Verification

  • Not run locally; this workspace is on main and does not contain the new packages/claude_code package.
  • Verified via GitHub data that current PR head is 12be8576faae85f4cdbe06eeb7f7a20e9aeca939, the README now contains the regenerated for example text, and Buildkite has started build #45097 for that head commit.

Follow-up

  • Watch build #45097. If it fails, investigate that newer log rather than this stale-docs failure from #45096.

What is this? | From workflow: PR Buildkite Detective

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@andrewkroh andrewkroh added New Integration Issue or pull request for creating a new integration package. documentation Improvements or additions to documentation. Applied to PRs that modify *.md files. and removed Integration:claude_code [Integration not found in source] labels Jun 26, 2026
@vera-review-bot

Copy link
Copy Markdown

👀 I have started reviewing the PR

@vera-review-bot

Copy link
Copy Markdown

Vera Review Bot

For the current commit state, I did not find any issues.


🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills

⚠️ Automated review — verify suggestions before applying.

@efd6 efd6 force-pushed the 19407-claude_code branch from bafa939 to 0888187 Compare June 30, 2026 02:44
@vera-review-bot

Copy link
Copy Markdown

👀 I have started reviewing the PR

1 similar comment
@vera-review-bot

Copy link
Copy Markdown

👀 I have started reviewing the PR

type: keyword
description: Name of the marketplace entry.
# OTel instrumentation scope
- name: scope.name

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 LOW data_stream/events/fields/fields.yml:272

scope. fields declared under wrong path*

These entries are nested inside the claude_code.events group, so they declare claude_code.events.scope.name and claude_code.events.scope.version. The pipeline only renames attributes to claude_code.events; it never moves the top-level OTel scope object. As sample_event.json shows, the actual fields land at the document root as scope.name / scope.version. The result is that the declared claude_code.events.scope.* fields are never populated, and the real scope.* fields are undeclared (working only via dynamic mapping). The integration ingests fine, but the field definitions do not match where the data lands.

Recommendation:

Declare scope at the document root, outside the claude_code.events group, so the mappings match the emitted fields:

- name: scope.name
  type: keyword
  description: OTel instrumentation scope name.
- name: scope.version
  type: keyword
  description: OTel instrumentation scope version.

🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills

⚠️ Automated review — verify suggestions before applying.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is half right. The claude_code.events.scope fields should be removed, but the scope fields are defined due to OTel.

}
handleMap(ctx);

- set:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 LOW data_stream/events/.../default.yml:425

No @​custom pipeline hook for user extension

The pipeline ends with the error-handling processors and has no call to a @​custom pipeline. Without that hook, an operator who needs to add their own enrichment or field tweaks has to edit the managed pipeline directly (lost on upgrade). The @​custom pipeline is the standard extensibility mechanism for integration ingest pipelines and is conventionally included as the final processing step on new data streams.

Recommendation:

Add a @​custom pipeline call as the last processing step (before the null-removal/error-tagging block, or at the end of the main processing), so user customizations survive upgrades:

- pipeline:
    tag: pipeline_custom
    name: '{{ IngestPipeline "@​custom" }}'
    ignore_missing_pipeline: true

🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills

⚠️ Automated review — verify suggestions before applying.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is not correct. @custom calls are injected by fleet at install time.

Replace the Painless flattening script with dot_expander + rename to
move all OTel custom attributes into a claude_code.* namespace. This
prevents semantically different vendor fields from colliding with ECS
or other integration fields at the root level.

Other changes in this commit:

- Add type conversion processors for numeric/boolean attributes.
- Map event_name to event.action (ECS) and remove from root.
- Update all dashboards to filter on event.action.
- Add optional event.original capture via Json.dump (gated on
  preserve_original_event tag).
- Move scope.name/scope.version into claude_code.scope.*.
- Remove redundant OTel infrastructure fields (body, observed_timestamp).
- Retain resource.attributes for OTel passthrough mapping.
@efd6 efd6 force-pushed the 19407-claude_code branch from 0888187 to 52cca56 Compare June 30, 2026 04:21
@elastic-vault-github-plugin-prod

Copy link
Copy Markdown

✅ All changelog entries have the correct PR link.

@vera-review-bot

Copy link
Copy Markdown

👀 I have started reviewing the PR

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

💚 Build Succeeded

History

cc @efd6


Run the [Elastic Distribution of the OpenTelemetry Collector](https://github.com/elastic/elastic-agent) with an `otlp` receiver and an `elasticsearch` exporter. Configure the `data_stream.dataset` resource attribute as above. The collector routes events to `logs-claude_code.events.otel-*`.

### Option C: Managed OTLP (mOTLP)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@efd6 in the interest of giving users an opinionated "best" option, could mOTLP be positioned as the recommended approach, since it avoids users having to deploy/manage agents? Regarding the user flow for this option - do they need to install the integration assets in Fleet first (for the mappings/pipeline/dashboards) before pointing Claude Code at the mOTLP endpoint?

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

Labels

documentation Improvements or additions to documentation. Applied to PRs that modify *.md files. enhancement New feature or request New Integration Issue or pull request for creating a new integration package. Team:Security-Service Integrations Security Service Integrations team [elastic/security-service-integrations]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants