Skip to content

Commit 31c78b3

Browse files
epinzurSprite
andauthored
feat(core): Add unified observability type system and context propagation (#13058)
## Summary - Introduces a new observability type system in `packages/core/src/observability/types/` with interfaces for logging (`LoggerContext`), metrics (`MetricsContext`, `Counter`, `Gauge`, `Histogram`), scores (`ScoreInput`, `ExportedScore`), feedback (`FeedbackInput`, `ExportedFeedback`), and an event bus (`ObservabilityEventBus`) - Creates `ObservabilityContext` — a unified interface combining `tracing`, `loggerVNext`, `metrics`, and `tracingContext` (alias) — that all internal execution contexts now extend - Adds `createObservabilityContext()` factory and `resolveObservabilityContext()` resolver with no-op defaults for graceful degradation - Refactors `tracing.ts` to extract a `SpanData` base interface, split `RecordedSpan`/`RecordedTrace` (immutable storage types) from live `Span` interfaces, and reorganize core infrastructure types into `core.ts` - Migrates 60+ sites across tools, workflows, agents, evals, processors, LLM, and stream systems to use `ObservabilityContext` (or `Partial<>` at public boundaries) instead of the former `TracingContext`. - Establishes a consistent forwarding pattern using `...rest` destructuring + `resolveObservabilityContext()` so observability fields flow through without listing them individually - Tightens internal-only sites from `Partial<ObservabilityContext>` to full `ObservabilityContext` so future logging/metrics additions won't require signature changes - Adds `loggerVNext` and `metrics` getters to the `Mastra` class ## Motivation This PR lays the groundwork for unified observability in Mastra. Today, only tracing (`TracingContext`) flows through execution contexts. Logging is ad-hoc via `IMastraLogger`, and there is no metrics system at all. Neither logging nor metrics are available in tool/workflow/processor execute callbacks. By establishing the type system and context plumbing now (all within `@mastra/core`), subsequent PRs can add concrete implementations (event bus, storage adapters, exporters) without touching interface signatures across the codebase. ## Design decisions **`loggerVNext` naming**: Uses VNext suffix to avoid collision with the existing `logger: IMastraLogger` infrastructure logger used throughout the codebase (`MastraPrimitives.logger`, `LoopOptions.logger`, `MastraBase.logger`). Follows established codebase pattern: `MastraLLMVNext`, `streamVNext`, `generateVNext`. **Partial vs Full boundary**: Public-facing APIs (tool execute, workflow step execute, processor context) use `Partial<ObservabilityContext>` — users shouldn't need full observability to call APIs. Internal/infrastructure code uses full `ObservabilityContext` — guarantees tracing/logging/metrics are always available. **Source vs Derived**: `tracingContext` is the source (position in span tree). `loggerVNext` and `metrics` are derived (rebuilt from current span for correlation). **Forwarding pattern**: To avoid listing `tracingContext`, `tracing`, `loggerVNext`, `metrics` individually at every call site, all forwarding uses a two-step pattern: 1. Destructure known fields + `...rest` to capture the observability fields 2. `const observabilityContext = resolveObservabilityContext(rest)` to fill in defaults 3. Forward via `...observabilityContext` spread at downstream calls This keeps forwarding sites concise and means adding a new observability signal requires zero changes at forwarding sites. **Span-creation boundaries left intact**: Sites that call `getOrCreateSpan()` or `createChildSpan()` to create a new span, then derive a fresh `ObservabilityContext` via `createObservabilityContext({ currentSpan })`, are intentionally not changed — they correctly establish new observability scopes rather than forwarding the parent's. ## What changed ### New files | File | Description | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `observability/types/core.ts` | `ObservabilityContext`, `ObservabilityInstance`, `ObservabilityExporter`, event bus, bridge, config interfaces | | `observability/types/logging.ts` | `LoggerContext`, `LogLevel`, `ExportedLog`, `LogEvent` | | `observability/types/metrics.ts` | `MetricsContext`, `Counter`, `Gauge`, `Histogram`, `ExportedMetric`, `MetricEvent`, cardinality config | | `observability/types/scores.ts` | `ScoreInput`, `ExportedScore`, `ScoreEvent` | | `observability/types/feedback.ts` | `FeedbackInput`, `ExportedFeedback`, `FeedbackEvent` | | `observability/context-factory.ts` | `createObservabilityContext()`, `resolveObservabilityContext()` | | `observability/context-factory.test.ts` | Unit tests for factory and resolver | ### Modified files (by area) **Observability** — Refactored `tracing.ts` (extracted `SpanData` base, split `RecordedSpan`/`RecordedTrace`), expanded `no-op.ts` with `noOpLoggerContext`/`noOpMetricsContext`, updated barrel exports **Agent** — `agent.ts` tool listing/conversion methods and sub-agent/workflow execution callbacks use `...observabilityFields` rest + `resolveObservabilityContext`; `agent-legacy.ts` `__primitive` and `prepareLLMOptions` use same pattern; `trip-wire.ts` `getModelOutputForTripwire` forwards full context; workflow steps (`prepare-memory-step.ts`, `stream-step.ts`, `map-results-step.ts`) use rest spread **Workflows** — `step.ts` removed redundant standalone `tracingContext: TracingContext` from `ExecuteFunctionParams` (already provided by `Partial<ObservabilityContext>`); `workflow.ts` agent-step and tool-step execute functions use `...observabilityFields` rest pattern; handlers (`entry.ts`, `step.ts`, `control-flow.ts`, `sleep.ts`) all use `...rest` + `resolveObservabilityContext(rest)` so `observabilityContext` is available for future logging/metrics; `evented/workflow.ts` creates context at processor call sites; `default.ts` passes context through execution engine **Evals** — `base.ts` scorer workflow uses rest spread; `run/index.ts` forwards full obs context to `executeWorkflow`, `executeAgent`, and all `scorer.run()` calls; `hooks.ts` `runScorer` requires full context; `scoreTracesWorkflow.ts` resolves at boundary **Processors** — `runner.ts` internal methods require full `ObservabilityContext`; boundary methods (`runProcessInputStep`, `runProcessOutputStep`) stay Partial; six processor implementations (`pii-detector`, `moderation`, `structured-output`, `language-detector`, `prompt-injection-detector`, `system-prompt-scrubber`) use `...observabilityFields` rest in public methods and accept full `ObservabilityContext` in private methods **Memory processors** — `message-history.ts` and `semantic-recall.ts` use `& Partial<ObservabilityContext>` on method signatures **Stream** — `output.ts` bridges Partial options to Full at `processPart`/`runOutputProcessors` calls via `resolveObservabilityContext()` **LLM** — `model.ts` methods destructure obs fields (with `_` prefix aliases to exclude from AI SDK `...rest`), create `observabilityContext` for internal use; `model.loop.ts`, `model.loop.types.ts`, `base.types.ts`, `llm/index.ts` extend context **Tools** — `types.ts` extends `Partial<ObservabilityContext>`; `builder.ts` uses `createObservabilityContext()` **Loop** — `types.ts` extends `Partial<ObservabilityContext>`; `network/index.ts` resolves at bridge site **Mastra class** — Added `loggerVNext` and `metrics` getters <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Unified ObservabilityContext with factory/resolver to provide tracing, structured logging, and metrics. * Mastra accessors for structured logging and metrics (loggerVNext, metrics). * New public types for logging, metrics, scoring, and feedback to surface observability signals. * **Chores** * Propagated ObservabilityContext across APIs and workflows for consistent telemetry. * Added no-op fallbacks and tests to ensure graceful degradation when observability implementations are absent. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Sprite <noreply@sprite.dev>
1 parent 09c3b18 commit 31c78b3

70 files changed

Lines changed: 2447 additions & 1221 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/thin-knives-accept.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@mastra/core': minor
3+
---
4+
5+
Added a unified observability type system with interfaces for structured logging, metrics (counters, gauges, histograms), scores, and feedback alongside the existing tracing infrastructure.
6+
7+
**Why?** Previously, only tracing flowed through execution contexts. Logging was ad-hoc and metrics did not exist. This change establishes the type system and context plumbing so that when concrete implementations land, logging and metrics will flow through execute callbacks automatically — no migration needed.
8+
9+
**What changed:**
10+
11+
- New `ObservabilityContext` interface combining tracing, logging, and metrics contexts
12+
- New type definitions for `LoggerContext`, `MetricsContext`, `ScoreInput`, `FeedbackInput`, and `ObservabilityEventBus`
13+
- `createObservabilityContext()` factory and `resolveObservabilityContext()` resolver with no-op defaults for graceful degradation
14+
- Future logging and metrics signals will propagate automatically through execution contexts — no migration needed
15+
- Added `loggerVNext` and `metrics` getters to the `Mastra` class

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,5 @@ test-output.log
5656
.playwright-mcp
5757
om-debug.log
5858
om-reflector-fixture.json
59+
.notion
60+
.dev/lima

packages/core/src/agent/__tests__/scorers.test.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -243,26 +243,28 @@ function scorersTests(version: 'v1' | 'v2') {
243243
}
244244

245245
// Verify the exact call parameters
246-
expect(runScorer).toHaveBeenCalledWith({
247-
scorerId: 'scorer-1',
248-
scorerObject: expect.objectContaining({
249-
scorer: scorer1,
250-
}),
251-
runId: expect.any(String),
252-
input: expect.any(Object),
253-
output: expect.any(Object),
254-
requestContext: expect.any(Object),
255-
entity: expect.objectContaining({
256-
id: 'test-agent',
257-
name: 'Test Agent',
246+
expect(runScorer).toHaveBeenCalledWith(
247+
expect.objectContaining({
248+
scorerId: 'scorer-1',
249+
scorerObject: expect.objectContaining({
250+
scorer: scorer1,
251+
}),
252+
runId: expect.any(String),
253+
input: expect.any(Object),
254+
output: expect.any(Object),
255+
requestContext: expect.any(Object),
256+
entity: expect.objectContaining({
257+
id: 'test-agent',
258+
name: 'Test Agent',
259+
}),
260+
source: 'LIVE',
261+
entityType: 'AGENT',
262+
structuredOutput: false,
263+
threadId: undefined,
264+
resourceId: undefined,
265+
tracingContext: expect.any(Object),
258266
}),
259-
source: 'LIVE',
260-
entityType: 'AGENT',
261-
structuredOutput: false,
262-
threadId: undefined,
263-
resourceId: undefined,
264-
tracingContext: expect.any(Object),
265-
});
267+
);
266268
});
267269
});
268270
}

0 commit comments

Comments
 (0)