Skip to content

feat(instrumentation-google-generativeai): add OTel 1.40 GenAI semantic conventions instrumentation#931

Merged
lenatraceloop merged 2 commits intomainfrom
lena/TLP-2077-instrumentation-google-generativeai-otel-1.40
Apr 16, 2026
Merged

feat(instrumentation-google-generativeai): add OTel 1.40 GenAI semantic conventions instrumentation#931
lenatraceloop merged 2 commits intomainfrom
lena/TLP-2077-instrumentation-google-generativeai-otel-1.40

Conversation

@lenatraceloop
Copy link
Copy Markdown
Member

@lenatraceloop lenatraceloop commented Apr 14, 2026

Summary

  • Creates new @traceloop/instrumentation-google-generativeai — OpenTelemetry instrumentation for the @google/genai SDK, implementing OTel GenAI semantic conventions v1.40
  • Adds mapGenAIContentBlock to @traceloop/instrumentation-utils — a shared content block mapper for Google GenAI Part types (text, reasoning, blob, uri, tool_call, tool_call_response, executable_code, code_execution_result)
  • Registers the instrumentation in @traceloop/node-server-sdk

Attributes captured (aligned with OTel 1.40 upstream):

  • gen_ai.request.stop_sequences — stored as native string[]
  • gen_ai.usage.cache_read.input_tokens — from usageMetadata.cachedContentTokenCount
  • gen_ai.usage.reasoning_tokens — from usageMetadata.thoughtsTokenCount (thinking models)
  • gen_ai.request.thinking.budget_tokens — from thinkingConfig.thinkingBudget

Test coverage: 100% — 131 tests across 4 test files:

  • instrumentation.test.ts — end-to-end span attribute coverage (generateContent + streaming, traceContent on/off, tool use, system instructions, thinking config, token usage)
  • content_parts.test.ts — all Part types including edge cases (video modality, undefined response, string response)
  • finish_reasons.test.ts — all Gemini finish reason mappings for both span-level and per-message values
  • semconv.test.ts — all OTel 1.40 attribute constants

Fixes TLP-2077

Test plan

  • pnpm nx test @traceloop/instrumentation-google-generativeai — 131 tests passing
  • pnpm nx build @traceloop/instrumentation-google-generativeai — builds cleanly
  • pnpm nx run-many -t build — all 17 packages build
  • pnpm nx run-many -t test — all tests pass (qdrant requires a running Qdrant instance, pre-existing)

Summary by CodeRabbit

  • New Features

    • Added OpenTelemetry instrumentation for Google Generative AI with automatic spans, attribute mapping, content tracing toggle, and optional exception logging.
    • Added content-block mapping and standardized finish-reason mapping (includes unspecified → empty string).
    • Integrated Google GenAI instrumentation into Traceloop SDK with manual/automatic activation.
  • Documentation

    • Added README with setup and usage examples for the new instrumentation.
  • Tests

    • Added extensive tests covering formatting, finish reasons, streaming, span behavior, and error handling.
  • Chores

    • Added build, bundling, lint, and TypeScript configs plus packaging updates and tooling ignores.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding OpenTelemetry 1.40 GenAI semantic conventions instrumentation for Google Generative AI. It is specific, concise, and directly reflects the main objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lena/TLP-2077-instrumentation-google-generativeai-otel-1.40

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/instrumentation-utils/eslint.config.cjs (1)

17-28: Drop no-op override entries to keep config lean.

The empty override blocks are redundant and can be removed without behavior change.

♻️ Suggested cleanup
 module.exports = [
   {
     ignores: ["!**/*", "**/node_modules", "dist/**/*"],
   },
   ...rootConfig,
-  {
-    files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
-    rules: {},
-  },
-  {
-    files: ["**/*.ts", "**/*.tsx"],
-    rules: {},
-  },
-  {
-    files: ["**/*.js", "**/*.jsx"],
-    rules: {},
-  },
 ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/instrumentation-utils/eslint.config.cjs` around lines 17 - 28,
Remove the three no-op override blocks in the ESLint config (the objects with
files: ["**/*.ts","**/*.tsx","**/*.js","**/*.jsx"], files:
["**/*.ts","**/*.tsx"], and files: ["**/*.js","**/*.jsx"]) since they contain
empty rules and add no behavior; delete those entire override entries to keep
packages/instrumentation-utils/eslint.config.cjs lean and functionally
equivalent.
packages/instrumentation-google-generativeai/eslint.config.cjs (1)

17-28: Remove redundant empty override blocks.

These blocks do not change lint behavior and can be dropped to reduce config noise.

♻️ Suggested cleanup
 module.exports = [
   {
     ignores: ["!**/*", "**/node_modules", "dist/**/*"],
   },
   ...rootConfig,
-  {
-    files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
-    rules: {},
-  },
-  {
-    files: ["**/*.ts", "**/*.tsx"],
-    rules: {},
-  },
-  {
-    files: ["**/*.js", "**/*.jsx"],
-    rules: {},
-  },
 ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/instrumentation-google-generativeai/eslint.config.cjs` around lines
17 - 28, Remove the redundant empty override blocks in the ESLint config: delete
the extra override objects that contain only files: ["**/*.ts", "**/*.tsx",
"**/*.js", "**/*.jsx"] with rules: {} and the separate overrides for
["**/*.ts","**/*.tsx"] and ["**/*.js","**/*.jsx"]; leave only the necessary
override(s) or consolidate them so no empty override objects remain in
eslint.config.cjs.
packages/instrumentation-utils/src/content-block-mappers.ts (1)

299-307: Harden mapper input handling before property access.

block.thought is dereferenced without guarding nullish/non-object values, which can throw on malformed input.

🛡️ Suggested hardening
 export function mapGenAIContentBlock(block: any): object {
   if (typeof block === "string") {
     return { type: GenAIOtelPartType.TEXT, content: block };
   }
+  if (!block || typeof block !== "object") {
+    return { type: GenAIOtelPartType.UNKNOWN };
+  }

   // thought: true marks a model reasoning/thinking block (Gemini thinking models).
   // text may be undefined on malformed parts — fall back to empty string.
   if (block.thought === true) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/instrumentation-utils/src/content-block-mappers.ts` around lines 299
- 307, The mapper mapGenAIContentBlock accesses block.thought without guarding
against null/primitive/malformed inputs, so first ensure block is an object
(e.g., check block !== null && typeof block === "object") before reading
properties; if the input is not an object, treat it as a TEXT part (or return {
type: GenAIOtelPartType.TEXT, content: "" } / fallback as appropriate). Update
mapGenAIContentBlock to validate block is an object before the block.thought
check and use optional chaining or nullish coalescing for block.text (e.g.,
block.text ?? "") when building REASONING or TEXT outputs to avoid runtime
exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/instrumentation-google-generativeai/README.md`:
- Line 4: The README's badges are linking incorrectly: update the license badge
so the image links to the repository license (e.g., wrap the license image
markdown in a link to your LICENSE file or the Apache license URL) instead of
linking to the image itself, and fix the npm badge target by using the correct
package slug in the shields.io URL (ensure the npm badge's href and image URL
use the actual package name used by this package). Locate the badge lines in
README.md and replace the current image-only link with a linked badge pointing
to the LICENSE or Apache URL and correct the npm badge's slug to match this
package.

---

Nitpick comments:
In `@packages/instrumentation-google-generativeai/eslint.config.cjs`:
- Around line 17-28: Remove the redundant empty override blocks in the ESLint
config: delete the extra override objects that contain only files: ["**/*.ts",
"**/*.tsx", "**/*.js", "**/*.jsx"] with rules: {} and the separate overrides for
["**/*.ts","**/*.tsx"] and ["**/*.js","**/*.jsx"]; leave only the necessary
override(s) or consolidate them so no empty override objects remain in
eslint.config.cjs.

In `@packages/instrumentation-utils/eslint.config.cjs`:
- Around line 17-28: Remove the three no-op override blocks in the ESLint config
(the objects with files: ["**/*.ts","**/*.tsx","**/*.js","**/*.jsx"], files:
["**/*.ts","**/*.tsx"], and files: ["**/*.js","**/*.jsx"]) since they contain
empty rules and add no behavior; delete those entire override entries to keep
packages/instrumentation-utils/eslint.config.cjs lean and functionally
equivalent.

In `@packages/instrumentation-utils/src/content-block-mappers.ts`:
- Around line 299-307: The mapper mapGenAIContentBlock accesses block.thought
without guarding against null/primitive/malformed inputs, so first ensure block
is an object (e.g., check block !== null && typeof block === "object") before
reading properties; if the input is not an object, treat it as a TEXT part (or
return { type: GenAIOtelPartType.TEXT, content: "" } / fallback as appropriate).
Update mapGenAIContentBlock to validate block is an object before the
block.thought check and use optional chaining or nullish coalescing for
block.text (e.g., block.text ?? "") when building REASONING or TEXT outputs to
avoid runtime exceptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d532b6f2-e25e-4d88-9f2f-606cb5c32ef9

📥 Commits

Reviewing files that changed from the base of the PR and between 49a1544 and 62a939f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • packages/ai-semantic-conventions/src/SemanticAttributes.ts
  • packages/instrumentation-google-generativeai/.prettierignore
  • packages/instrumentation-google-generativeai/README.md
  • packages/instrumentation-google-generativeai/eslint.config.cjs
  • packages/instrumentation-google-generativeai/package.json
  • packages/instrumentation-google-generativeai/rollup.config.js
  • packages/instrumentation-google-generativeai/src/index.ts
  • packages/instrumentation-google-generativeai/src/instrumentation.ts
  • packages/instrumentation-google-generativeai/src/types.ts
  • packages/instrumentation-google-generativeai/tests/content_parts.test.ts
  • packages/instrumentation-google-generativeai/tests/finish_reasons.test.ts
  • packages/instrumentation-google-generativeai/tests/instrumentation.test.ts
  • packages/instrumentation-google-generativeai/tests/semconv.test.ts
  • packages/instrumentation-google-generativeai/tsconfig.json
  • packages/instrumentation-google-generativeai/tsconfig.test.json
  • packages/instrumentation-utils/eslint.config.cjs
  • packages/instrumentation-utils/src/content-block-mappers.ts
  • packages/instrumentation-utils/src/index.ts
  • packages/instrumentation-utils/src/message-formatters.ts
  • packages/traceloop-sdk/package.json
  • packages/traceloop-sdk/src/lib/interfaces/initialize-options.interface.ts
  • packages/traceloop-sdk/src/lib/tracing/index.ts

Comment thread packages/instrumentation-google-generativeai/README.md Outdated
@lenatraceloop lenatraceloop force-pushed the lena/TLP-2077-instrumentation-google-generativeai-otel-1.40 branch from 8854a07 to bdd7d47 Compare April 14, 2026 14:54
Comment thread packages/instrumentation-google-generativeai/src/instrumentation.ts Outdated
}

private wrapGoogleGenAI() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is the eslint disable needed in this file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This pattern is used consistently across all instrumentations in this codebase — it's the standard way wrappers are written here.

@@ -0,0 +1,6 @@
import { InstrumentationConfig } from "@opentelemetry/instrumentation";

export interface GenAIInstrumentationConfig extends InstrumentationConfig {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this in use?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, it's used — _config.traceContent and _config.exceptionLogger. The type declaration is needed so TypeScript knows about these custom fields.

Comment thread packages/instrumentation-utils/src/content-block-mappers.ts Outdated
@lenatraceloop lenatraceloop force-pushed the lena/TLP-2077-instrumentation-google-generativeai-otel-1.40 branch 3 times, most recently from 77de256 to 6081443 Compare April 15, 2026 09:08
Comment thread packages/traceloop-sdk/package.json Outdated
Copy link
Copy Markdown
Contributor

@OzBenSimhonTraceloop OzBenSimhonTraceloop left a comment

Choose a reason for hiding this comment

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

P1 — Must Fix

  1. Span-level finish_reasons uses passthrough lowercase instead of OTel mapped values
    instrumentation.ts:88-91, instrumentation.ts:468-476

geminiReasonToSpanValue() lowercases vendor values (SAFETY → "safety", MAX_TOKENS → "max_tokens"). Should use genaiFinishReasonMap like Anthropic/OpenAI instrumentations do, producing OTel canonical values ("content_filter", "length"). Traceloop Python uses one mapper for both paths — no dual-path design. Delete geminiReasonToSpanValue, use genaiFinishReasonMap with ?? r fallback.

  1. Streaming accumulation merges thinking + regular text
    instrumentation.ts:293-302

Consecutive text parts concatenated without checking thought flag. A {text: "...", thought: true} followed by {text: "..."} merges into one reasoning part. Fix: add && !!prev.thought === !!part.thought to the concat condition.

P2 — Should Fix
3. OTHER maps to "other" — should be "error" (cross-SDK drift)
instrumentation.ts:74

Traceloop Python and upstream OTel Python both map OTHER → "error". JS maps to "other" which isn't in the OTel FinishReason enum. Fix: OTHER: FinishReasons.ERROR.

  1. BlobPart/UriPart modality omitted when mimeType absent
    content-block-mapper.ts:72-78

modality is required per OTel schema. Fall back to "document" when mimeType is unknown.

Cross-SDK Drift (Python vs JS)
5. Provider name: gcp.gen_ai (Python) vs gcp.gemini (JS)
Both valid OTel values but must agree. gcp.gemini is more specific for AI Studio API. Align across SDKs.

  1. Top-level finish_reasons — per-candidate alignment vs dedup
    Python preserves per-candidate alignment (no dedup, "" placeholders, omits attr only when all empty). JS deduplicates via new Set() and drops nulls. Align on Python's approach — array length should match candidate count.

P3 — Nice to Have
7. Missing test: per-message finish_reason in multi-candidate output — test at line 597 checks span attr but not outputMessages[*].finish_reason.

  1. Missing test: streaming + thinking parts — would catch P1 #2.

  2. Missing test: streaming where final chunk has null finishReason — verify span attr omitted and output message has finish_reason: "".

  3. Empty string filter on span finish_reasons — filter((r) => r != null) doesn't exclude "". Add && r !== "" defensively.

@lenatraceloop
Copy link
Copy Markdown
Member Author

Thanks for the detailed review! All issues have been addressed:

P1 — Fixed:

  1. Deleted geminiReasonToSpanValue, now using genaiFinishReasonMap[r] ?? r everywhere — produces OTel canonical values ("content_filter", "length", etc.)
  2. Added && !!prev.thought === !!part.thought to the streaming concat condition — thinking and regular text parts are now kept separate

P2 — Fixed:
3. OTHER now maps to FinishReasons.ERROR — aligned with Python and upstream OTel
4. modality is now always set on BlobPart/UriPart, falling back to "document" when mimeType is absent
5. Provider name changed from "gcp.gemini" to "gcp.gen_ai" — aligned with Python instrumentation and OTel contrib
6. Removed new Set() dedup, finish_reasons array is now per-candidate with "" placeholders, attribute omitted only when all values are empty — matches Python behavior exactly

P3 — Done:
7. Added finish_reason assertions to the multi-candidate output messages test
8. Added streaming + thinking parts test
9. Added streaming with null finishReason test — verifies span attr is omitted and finish_reason: ""
10. Resolved via the .some(r => r !== "") condition

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/instrumentation-google-generativeai/src/instrumentation.ts`:
- Around line 68-86: The genaiFinishReasonMap currently maps the provider key
"OTHER" to FinishReasons.ERROR and elsewhere falls back to returning raw vendor
reason strings; update genaiFinishReasonMap so "OTHER" maps to
FinishReasons.FINISH_REASON_UNSPECIFIED (not ERROR) and change the
unknown-reason fallback logic to return FinishReasons.FINISH_REASON_UNSPECIFIED
instead of the raw vendor value, ensuring all unmapped/unknown finish reasons
normalize to FinishReasons.FINISH_REASON_UNSPECIFIED; adjust any code that reads
from genaiFinishReasonMap or the finish-reason normalization path so it uses
FinishReasons.FINISH_REASON_UNSPECIFIED as the default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5fff7c80-1eac-4d65-976c-c0ace86a39dd

📥 Commits

Reviewing files that changed from the base of the PR and between 9019ed4 and 895382b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • packages/ai-semantic-conventions/src/SemanticAttributes.ts
  • packages/instrumentation-google-generativeai/.prettierignore
  • packages/instrumentation-google-generativeai/README.md
  • packages/instrumentation-google-generativeai/eslint.config.cjs
  • packages/instrumentation-google-generativeai/package.json
  • packages/instrumentation-google-generativeai/rollup.config.js
  • packages/instrumentation-google-generativeai/src/content-block-mapper.ts
  • packages/instrumentation-google-generativeai/src/index.ts
  • packages/instrumentation-google-generativeai/src/instrumentation.ts
  • packages/instrumentation-google-generativeai/src/types.ts
  • packages/instrumentation-google-generativeai/tests/content_parts.test.ts
  • packages/instrumentation-google-generativeai/tests/finish_reasons.test.ts
  • packages/instrumentation-google-generativeai/tests/instrumentation.test.ts
  • packages/instrumentation-google-generativeai/tests/semconv.test.ts
  • packages/instrumentation-google-generativeai/tsconfig.json
  • packages/instrumentation-google-generativeai/tsconfig.test.json
  • packages/instrumentation-utils/src/message-formatters.ts
  • packages/traceloop-sdk/package.json
  • packages/traceloop-sdk/src/lib/interfaces/initialize-options.interface.ts
  • packages/traceloop-sdk/src/lib/tracing/index.ts
✅ Files skipped from review due to trivial changes (13)
  • packages/instrumentation-google-generativeai/.prettierignore
  • packages/instrumentation-google-generativeai/tsconfig.test.json
  • packages/instrumentation-google-generativeai/tsconfig.json
  • packages/ai-semantic-conventions/src/SemanticAttributes.ts
  • packages/instrumentation-google-generativeai/README.md
  • packages/instrumentation-google-generativeai/src/index.ts
  • packages/instrumentation-google-generativeai/eslint.config.cjs
  • packages/instrumentation-google-generativeai/package.json
  • packages/instrumentation-google-generativeai/tests/finish_reasons.test.ts
  • packages/instrumentation-google-generativeai/tests/semconv.test.ts
  • packages/instrumentation-google-generativeai/tests/content_parts.test.ts
  • packages/instrumentation-google-generativeai/src/types.ts
  • packages/instrumentation-google-generativeai/tests/instrumentation.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/traceloop-sdk/src/lib/interfaces/initialize-options.interface.ts
  • packages/traceloop-sdk/package.json
  • packages/traceloop-sdk/src/lib/tracing/index.ts
  • packages/instrumentation-utils/src/message-formatters.ts
  • packages/instrumentation-google-generativeai/rollup.config.js
  • packages/instrumentation-google-generativeai/src/content-block-mapper.ts

Comment thread packages/instrumentation-google-generativeai/src/instrumentation.ts
@lenatraceloop lenatraceloop force-pushed the lena/TLP-2077-instrumentation-google-generativeai-otel-1.40 branch from aba3dbd to 197c84f Compare April 15, 2026 14:44
Comment thread packages/ai-semantic-conventions/src/SemanticAttributes.ts Outdated
@lenatraceloop lenatraceloop merged commit 877fc42 into main Apr 16, 2026
7 of 9 checks passed
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.

3 participants