Skip to content

Add modular Langfuse-backed AI observability#82

Open
oshorefueled wants to merge 6 commits intomainfrom
feat/langfuse
Open

Add modular Langfuse-backed AI observability#82
oshorefueled wants to merge 6 commits intomainfrom
feat/langfuse

Conversation

@oshorefueled
Copy link
Copy Markdown
Contributor

@oshorefueled oshorefueled commented Apr 15, 2026

Title

Add modular Langfuse-backed AI observability

Summary

This adds a modular AI observability layer to VectorLint and wires it into the existing Vercel AI SDK execution paths. Langfuse is supported through in-process OpenTelemetry setup, observability failures stay non-blocking, and the repo now documents the new config plus the Node 20.6+ runtime requirement introduced by the Langfuse/OTEL dependency path.

Why

VectorLint had no structured way to observe AI executions, and wiring Langfuse directly into provider code would have coupled the core runtime to one backend. This adds a narrow observability seam so Langfuse can be enabled now without locking the architecture to a single platform.

Scope

  • In scope:
    • Add an AIObservability contract with noop and Langfuse-backed implementations
    • Inject observability into structured and agent-loop AI SDK calls
    • Initialize and flush observability in the CLI lifecycle
    • Add env/config validation, tests, and user-facing docs for Langfuse setup
    • Bump the declared Node runtime to 20.6+
  • Out of scope:
    • Instrumenting PerplexitySearchProvider
    • Adding a generic event bus, sidecar process, or redaction layer
    • Supporting multiple observability backends in this PR

Behavior Impact

  • User-facing changes: yes - users can enable Langfuse observability with OBSERVABILITY_BACKEND=langfuse and the corresponding credentials
  • Breaking changes: yes - the declared runtime support moves from Node 18+ to Node 20.6+
  • Operational impact: Langfuse config is optional and best-effort; when enabled it records prompts and outputs for VercelAIProvider calls and attempts a shutdown flush before process exit

Risk and Mitigations

  • Risk level: medium
  • Primary risks:
    • Node runtime compatibility changed to satisfy the Langfuse/OTEL dependency chain
    • CLI exit flow now includes observability init/shutdown handling
    • Observability validation exists in env parsing and a defensive runtime guard in factory
  • Mitigations:
    • Added focused tests for env parsing, factory selection, provider wiring, Langfuse behavior, and CLI lifecycle
    • Kept observability failures non-blocking with noop fallback and warning logs
    • Pass provider/model identity explicitly through provider config instead of reflecting on model internals
    • Verified the full suite with lint, build, and vitest run
  • Rollback plan:
    • Disable Langfuse by removing OBSERVABILITY_BACKEND
    • Revert this branch to restore the previous no-observability path and Node 18+ engine declaration

API / Contract / Schema Changes

  • New env vars:
    • OBSERVABILITY_BACKEND=langfuse
    • LANGFUSE_PUBLIC_KEY
    • LANGFUSE_SECRET_KEY
    • optional LANGFUSE_BASE_URL
  • New internal contract:
    • AIExecutionContext
    • AIObservability
  • Runtime requirement:
    • package.json engines updated to >=20.6.0

Follow-ups

  • Consider whether PerplexitySearchProvider should also participate in the observability layer
  • Consider collapsing duplicate Langfuse validation to env parsing as the single source of truth
  • Consider a stricter lifecycle refactor in src/cli/commands.ts to remove mutable exit bookkeeping

Known Tradeoffs

  • The first implementation is intentionally narrow and only instruments VercelAIProvider
  • Prompts and outputs are recorded when Langfuse observability is enabled

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

This PR introduces optional Langfuse-based OpenTelemetry observability integration to the codebase. It adds observability interfaces, factory implementations, CLI lifecycle management, provider decoration logic, environment validation, and comprehensive test coverage. Observability initialization is non-blocking with graceful fallback to no-op on failure.

Changes

Cohort / File(s) Summary
Documentation & Configuration
.env.example, CONFIGURATION.md, README.md
Added observability section documenting optional Langfuse telemetry configuration (OBSERVABILITY_BACKEND, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL). Updated Node.js requirement to 20.6+. Specified best-effort/non-blocking telemetry behavior and prompt/output recording capabilities.
Package Dependencies
package.json
Raised minimum Node.js version from >=18.0.0 to >=20.6.0. Added production dependencies: @langfuse/otel, @opentelemetry/api, @opentelemetry/sdk-node, langfuse.
Observability Infrastructure
src/observability/ai-observability.ts, src/observability/noop-observability.ts, src/observability/langfuse-observability.ts
Defined core AIObservability and AIExecutionContext interfaces. Implemented NoopObservability (empty decorator) and LangfuseObservability (OpenTelemetry SDK lifecycle management with call decoration via experimental_telemetry).
Observability Factory
src/observability/factory.ts
Created createObservability(env, logger?) factory function that selects backend based on OBSERVABILITY_BACKEND env var; validates Langfuse credentials when enabled; returns NoopObservability otherwise.
Provider Integration
src/providers/provider-factory.ts, src/providers/vercel-ai-provider.ts
Extended ProviderOptions and VercelAIConfig to accept optional observability instance. Injected call decoration into Vercel generateText requests for structured evaluation and agent tool loops with context-specific metadata.
CLI Lifecycle
src/cli/commands.ts
Integrated observability initialization before provider creation via new initializeObservability helper. Wrapped evaluation in try/finally to ensure shutdown() is called. Deferred process exit to after shutdown completion.
Environment Validation
src/boundaries/env-parser.ts, src/schemas/env-schemas.ts
Added OBSERVABILITY_ENV_SCHEMA with optional Langfuse variables. Enforced conditional validation: LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY required when OBSERVABILITY_BACKEND='langfuse'. Enhanced error messages listing missing Langfuse variables. Expanded provider discriminator to include gemini and amazon-bedrock.
Unit Tests
tests/observability/*.test.ts
Added test suites for NoopObservability, LangfuseObservability (initialization, decoration, shutdown, error handling), and createObservability factory (backend selection, validation, credential handling).
Integration Tests
tests/main-command-observability.test.ts, tests/provider-factory.test.ts, tests/providers/vercel-ai-provider-agent-loop.test.ts, tests/vercel-ai-provider.test.ts
Added CLI observability lifecycle tests, provider factory wiring test, and VercelAIProvider decoration tests (structured/agent-loop operations, error handling with graceful degradation).
Environment Parser Tests
tests/env-parser.test.ts
Updated provider validation test for expanded LLM provider list. Added observability configuration test suite verifying Langfuse variable parsing and validation error messages.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Main
    participant ObsFactory as Observability Factory
    participant Obs as AIObservability
    participant Provider as VercelAIProvider
    participant VSDKas Vercel AI SDK
    participant LF as Langfuse/OTEL

    CLI->>ObsFactory: createObservability(env, logger)
    ObsFactory->>Obs: new LangfuseObservability(config)
    CLI->>Obs: init()
    Obs->>LF: NodeSDK.start()
    LF-->>Obs: initialized
    
    CLI->>Provider: createProvider(env, {observability})
    Provider-->>CLI: provider instance
    
    CLI->>Provider: runPromptStructured(...)
    Provider->>Provider: getObservabilityOptions(context)
    Provider->>Obs: decorateCall({operation, provider, model, evaluator, rule})
    Obs-->>Provider: {experimental_telemetry: {...}}
    Provider->>VSDKas: generateText(..., experimental_telemetry)
    VSDKas->>LF: record call with telemetry
    LF-->>VSDKas: response
    VSDKas-->>Provider: result
    Provider-->>CLI: evaluation complete

    CLI->>Obs: shutdown()
    Obs->>LF: NodeSDK.shutdown()
    LF-->>Obs: shutdown complete
    CLI->>CLI: process.exit(code)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

Suggested reviewers

  • ayo6706

Poem

🐰 Hops through the code with telemetry glow,
Langfuse trails marking where the prompts flow,
OTEL's span processor, now shining so bright,
Observability's magic—no-op when not right!
Graceful degradation, the best kind of sight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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
Title check ✅ Passed The title 'Add modular Langfuse-backed AI observability' accurately and concisely summarizes the main objective of the PR: introducing a modular observability layer with Langfuse integration.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 feat/langfuse

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

@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 (4)
README.md (1)

143-146: Add an explicit sensitive-data caution in observability notes.

Since prompts/outputs are recorded, add a short warning to avoid sending secrets/PII unless organizational policy allows it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 143 - 146, Update the Observability notes block in
README.md (the "Observability is non-blocking..." / "Prompts and outputs are
recorded..." lines) to add a concise sensitive-data warning: a one-line caution
advising users not to include secrets, credentials, or PII in prompts/outputs
unless their organizational policy explicitly permits it, and flag that recorded
data may be accessible by observability tooling; keep the tone brief and add it
immediately after the existing recorded-data sentence.
src/schemas/env-schemas.ts (1)

66-71: Extract observability backend literals into a shared constant/type.

'langfuse' is repeated in schema and refinement checks; centralizing it reduces drift across runtime/config modules.

♻️ Suggested refactor
+export const OBSERVABILITY_BACKENDS = ['langfuse'] as const;
+export type ObservabilityBackend = (typeof OBSERVABILITY_BACKENDS)[number];
+
 const OBSERVABILITY_ENV_SCHEMA = z.object({
-  OBSERVABILITY_BACKEND: z.enum(['langfuse']).optional(),
+  OBSERVABILITY_BACKEND: z.enum(OBSERVABILITY_BACKENDS).optional(),
   LANGFUSE_PUBLIC_KEY: z.string().min(1).optional(),
   LANGFUSE_SECRET_KEY: z.string().min(1).optional(),
   LANGFUSE_BASE_URL: z.string().url().optional(),
 });
@@
-  if (data.OBSERVABILITY_BACKEND === 'langfuse') {
+  if (data.OBSERVABILITY_BACKEND === OBSERVABILITY_BACKENDS[0]) {

As per coding guidelines "Define shared domain constants, enums, or types for core runtime concepts; avoid magic strings in shared runtime code".

Also applies to: 98-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemas/env-schemas.ts` around lines 66 - 71, Define a shared
constant/type for observability backends (e.g., OBSERVABILITY_BACKENDS or
ObservabilityBackend) and replace the hard-coded literal 'langfuse' used in
OBSERVABILITY_ENV_SCHEMA and any related runtime/refinement checks (references:
OBSERVABILITY_ENV_SCHEMA and the refinements around lines ~98-114) with that
constant/type so both the z.enum(...) and all runtime validations import and use
the same source of truth; update the schema definition to reference the shared
constant values and change the refinement/validation code to use the same
exported enum/union to avoid duplicate magic strings.
src/boundaries/env-parser.ts (1)

31-31: Derive the allowed provider list from a shared constant instead of hard-coding it here.

This message is now another place that must be updated whenever provider support changes. Please source the allowed values from the same shared runtime constant/enum that drives validation so the error text cannot drift again.

As per coding guidelines, "Define shared domain constants, enums, or types for core runtime concepts; avoid magic strings in shared runtime code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/boundaries/env-parser.ts` at line 31, The error message in env-parser.ts
hardcodes provider names; replace the literal list with a derived string from
the shared provider constant/enum used for validation (e.g.,
SUPPORTED_LLM_PROVIDERS or LLMProviderType) so the message cannot drift—import
that constant/enum, join its values to produce the allowed list, and use
providerType (as in the current return) for the received value in the final
string.
src/observability/factory.ts (1)

12-13: Use the repository error type here instead of throwing raw Error.

This is still a configuration validation failure, so surfacing it as a native exception makes observability init inconsistent with the rest of the boundary/error handling.

Suggested change
+import { ValidationError } from '../errors/index';
 import type { Logger } from '../logging/logger';
 import type { EnvConfig } from '../schemas/env-schemas';
 import type { AIObservability } from './ai-observability';
 import { LangfuseObservability } from './langfuse-observability';
 import { NoopObservability } from './noop-observability';
@@
   if (!env.LANGFUSE_PUBLIC_KEY || !env.LANGFUSE_SECRET_KEY) {
-    throw new Error('Langfuse observability requires LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY');
+    throw new ValidationError('Langfuse observability requires LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY');
   }

As per coding guidelines, "Prefer the repository's custom error hierarchy over native Error."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/observability/factory.ts` around lines 12 - 13, Replace the raw throw new
Error(...) in the observability init check with the repository's custom
configuration/error class: import the project's config/validation error type
(e.g., ConfigError or RepositoryError) from the central errors module and throw
that instead, using the same message that references env.LANGFUSE_PUBLIC_KEY and
env.LANGFUSE_SECRET_KEY so the validation failure is surfaced via the repo's
error hierarchy; ensure the import and thrown type replace the current throw new
Error call in the LANGFUSE key check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/boundaries/env-parser.ts`:
- Around line 62-69: The Langfuse branch currently treats any issue whose path
starts with "LANGFUSE_" as a missing credential; update the logic in the
env-parser branch that checks envObj.OBSERVABILITY_BACKEND === 'langfuse' (the
block that builds langfuseFields and missingLangfuseFields) to only
collect/report missing variables for the two credential keys LANGFUSE_PUBLIC_KEY
and LANGFUSE_SECRET_KEY (e.g., filter issue.path to exactly those two names and
only include issues with a "missing" type), and do not swallow other LANGFUSE_*
validation errors so they will be handled by the generic invalid-value formatter
instead. Ensure the message still references the two credential names and that
other Langfuse validation failures are left to the existing generic error path.

---

Nitpick comments:
In `@README.md`:
- Around line 143-146: Update the Observability notes block in README.md (the
"Observability is non-blocking..." / "Prompts and outputs are recorded..."
lines) to add a concise sensitive-data warning: a one-line caution advising
users not to include secrets, credentials, or PII in prompts/outputs unless
their organizational policy explicitly permits it, and flag that recorded data
may be accessible by observability tooling; keep the tone brief and add it
immediately after the existing recorded-data sentence.

In `@src/boundaries/env-parser.ts`:
- Line 31: The error message in env-parser.ts hardcodes provider names; replace
the literal list with a derived string from the shared provider constant/enum
used for validation (e.g., SUPPORTED_LLM_PROVIDERS or LLMProviderType) so the
message cannot drift—import that constant/enum, join its values to produce the
allowed list, and use providerType (as in the current return) for the received
value in the final string.

In `@src/observability/factory.ts`:
- Around line 12-13: Replace the raw throw new Error(...) in the observability
init check with the repository's custom configuration/error class: import the
project's config/validation error type (e.g., ConfigError or RepositoryError)
from the central errors module and throw that instead, using the same message
that references env.LANGFUSE_PUBLIC_KEY and env.LANGFUSE_SECRET_KEY so the
validation failure is surfaced via the repo's error hierarchy; ensure the import
and thrown type replace the current throw new Error call in the LANGFUSE key
check.

In `@src/schemas/env-schemas.ts`:
- Around line 66-71: Define a shared constant/type for observability backends
(e.g., OBSERVABILITY_BACKENDS or ObservabilityBackend) and replace the
hard-coded literal 'langfuse' used in OBSERVABILITY_ENV_SCHEMA and any related
runtime/refinement checks (references: OBSERVABILITY_ENV_SCHEMA and the
refinements around lines ~98-114) with that constant/type so both the
z.enum(...) and all runtime validations import and use the same source of truth;
update the schema definition to reference the shared constant values and change
the refinement/validation code to use the same exported enum/union to avoid
duplicate magic strings.
🪄 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: bf639973-83c8-40ff-bcf7-d1ddf9ebcd99

📥 Commits

Reviewing files that changed from the base of the PR and between eff7743 and 7f5924a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (21)
  • .env.example
  • CONFIGURATION.md
  • README.md
  • package.json
  • src/boundaries/env-parser.ts
  • src/cli/commands.ts
  • src/observability/ai-observability.ts
  • src/observability/factory.ts
  • src/observability/langfuse-observability.ts
  • src/observability/noop-observability.ts
  • src/providers/provider-factory.ts
  • src/providers/vercel-ai-provider.ts
  • src/schemas/env-schemas.ts
  • tests/env-parser.test.ts
  • tests/main-command-observability.test.ts
  • tests/observability/factory.test.ts
  • tests/observability/langfuse-observability.test.ts
  • tests/observability/noop-observability.test.ts
  • tests/provider-factory.test.ts
  • tests/providers/vercel-ai-provider-agent-loop.test.ts
  • tests/vercel-ai-provider.test.ts

Comment thread src/boundaries/env-parser.ts Outdated
Comment on lines +62 to +69
if (envObj.OBSERVABILITY_BACKEND === 'langfuse') {
const langfuseFields = issues
.filter((issue) => issue.path.length > 0 && String(issue.path[0]).startsWith('LANGFUSE_'))
.map((issue) => issue.path.join('.'));
const missingLangfuseFields = [...new Set(langfuseFields)];

if (missingLangfuseFields.length > 0) {
return `Missing required Langfuse observability environment variables: ${missingLangfuseFields.join(', ')}. When using OBSERVABILITY_BACKEND=langfuse, ensure LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are 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.

⚠️ Potential issue | 🟡 Minor

This Langfuse branch mislabels non-credential validation failures as “missing required variables.”

The filter matches any LANGFUSE_* issue path, so an invalid LANGFUSE_BASE_URL will also hit this branch and produce the wrong message. Restrict this path to the two custom missing-credential issues and let other Langfuse validation errors fall through to the generic invalid-value formatter.

Suggested narrowing
   if (envObj.OBSERVABILITY_BACKEND === 'langfuse') {
-    const langfuseFields = issues
-      .filter((issue) => issue.path.length > 0 && String(issue.path[0]).startsWith('LANGFUSE_'))
-      .map((issue) => issue.path.join('.'));
-    const missingLangfuseFields = [...new Set(langfuseFields)];
+    const missingLangfuseFields = [...new Set(
+      issues
+        .filter((issue) =>
+          issue.code === 'custom' &&
+          (issue.path[0] === 'LANGFUSE_PUBLIC_KEY' || issue.path[0] === 'LANGFUSE_SECRET_KEY')
+        )
+        .map((issue) => issue.path.join('.'))
+    )];
 
     if (missingLangfuseFields.length > 0) {
       return `Missing required Langfuse observability environment variables: ${missingLangfuseFields.join(', ')}. When using OBSERVABILITY_BACKEND=langfuse, ensure LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set.`;
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (envObj.OBSERVABILITY_BACKEND === 'langfuse') {
const langfuseFields = issues
.filter((issue) => issue.path.length > 0 && String(issue.path[0]).startsWith('LANGFUSE_'))
.map((issue) => issue.path.join('.'));
const missingLangfuseFields = [...new Set(langfuseFields)];
if (missingLangfuseFields.length > 0) {
return `Missing required Langfuse observability environment variables: ${missingLangfuseFields.join(', ')}. When using OBSERVABILITY_BACKEND=langfuse, ensure LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set.`;
if (envObj.OBSERVABILITY_BACKEND === 'langfuse') {
const missingLangfuseFields = [...new Set(
issues
.filter((issue) =>
issue.code === 'custom' &&
(issue.path[0] === 'LANGFUSE_PUBLIC_KEY' || issue.path[0] === 'LANGFUSE_SECRET_KEY')
)
.map((issue) => issue.path.join('.'))
)];
if (missingLangfuseFields.length > 0) {
return `Missing required Langfuse observability environment variables: ${missingLangfuseFields.join(', ')}. When using OBSERVABILITY_BACKEND=langfuse, ensure LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set.`;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/boundaries/env-parser.ts` around lines 62 - 69, The Langfuse branch
currently treats any issue whose path starts with "LANGFUSE_" as a missing
credential; update the logic in the env-parser branch that checks
envObj.OBSERVABILITY_BACKEND === 'langfuse' (the block that builds
langfuseFields and missingLangfuseFields) to only collect/report missing
variables for the two credential keys LANGFUSE_PUBLIC_KEY and
LANGFUSE_SECRET_KEY (e.g., filter issue.path to exactly those two names and only
include issues with a "missing" type), and do not swallow other LANGFUSE_*
validation errors so they will be handled by the generic invalid-value formatter
instead. Ensure the message still references the two credential names and that
other Langfuse validation failures are left to the existing generic error path.

- create the runtime logger only for verbose runs or active telemetry

- pass provider and model names explicitly into Vercel AI calls instead of reflecting on SDK model objects

- centralize Langfuse backend constants and narrow missing-credential reporting to the required keys

- document recorded-data caution and extend tests around logger and telemetry metadata behavior
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.

1 participant