Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions typescript/packages/traceai_mastra/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## [0.2.0] - 2026-06-03
### Feature
- Support Mastra v1 observability: `createFIObservability` / `createFIMastraExporter` wire Future AGI into the new `@mastra/observability` pipeline (Mastra v1 removed the `telemetry:` config the previous exporter relied on).
- Map Mastra spans to Future AGI's `gen_ai.*` conventions — span kind (`FISpanKind`) and `input.value` / `output.value` enrichment — so traces render fully in the Future AGI UI.
- Move the legacy v0.x `FITraceExporter` integration to the `@traceai/mastra/legacy` subpath.

## [0.1.0]
### Feature
- Initial release: `FITraceExporter` for Mastra v0.x (`telemetry:` config).
2 changes: 2 additions & 0 deletions typescript/packages/traceai_mastra/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module.exports = {
'^.+\\.ts$': 'ts-jest'
},
moduleNameMapper: {
// Source uses NodeNext `.js` import specifiers; map them back to the .ts source.
'^(\\.{1,2}/.*)\\.js$': '$1',
'^@traceai/fi-core$': '<rootDir>/../fi-core/src',
'^@traceai/fi-semantic-conventions$': '<rootDir>/../fi-semantic-conventions/src'
},
Expand Down
18 changes: 17 additions & 1 deletion typescript/packages/traceai_mastra/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@traceai/mastra",
"version": "0.1.0",
"version": "0.2.0",
"description": "OpenTelemetry utilities for Mastra",
"main": "./dist/src/index.js",
"module": "./dist/esm/index.js",
Expand All @@ -11,6 +11,11 @@
"types": "./dist/src/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/src/index.js"
},
"./legacy": {
"types": "./dist/src/legacy.d.ts",
"import": "./dist/esm/legacy.js",
"require": "./dist/src/legacy.js"
}
},
"scripts": {
Expand Down Expand Up @@ -42,15 +47,26 @@
"@traceai/vercel": "workspace:^"
},
"devDependencies": {
"@mastra/core": "^1.16.0",
"@mastra/observability": "^1.14.0",
"@mastra/otel-exporter": "^1.2.0",
"vitest": "^3.1.3"
},
"peerDependencies": {
"@mastra/core": ">=1.16.0",
"@mastra/observability": ">=1.0.0",
"@mastra/otel-exporter": ">=1.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.0.1",
"@opentelemetry/exporter-trace-otlp-proto": "^0.201.1",
"@opentelemetry/sdk-trace-base": "^2.0.1",
"@opentelemetry/semantic-conventions": "^1.34.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": { "optional": true },
"@opentelemetry/core": { "optional": true },
"@opentelemetry/semantic-conventions": { "optional": true }
},
"author": "Future AGI <no-reply@futureagi.com>",
"publishConfig": {
"access": "public"
Expand Down
149 changes: 149 additions & 0 deletions typescript/packages/traceai_mastra/src/FIMastraSpanExporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { BaseExporter } from "@mastra/observability";
import { SpanConverter } from "@mastra/otel-exporter";
import { SpanType } from "@mastra/core/observability";
import type { AnyExportedSpan, TracingEvent } from "@mastra/core/observability";
import { FISpanKind } from "@traceai/fi-semantic-conventions";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";

export interface FISpanExporterConfig {
/** Full OTLP traces endpoint URL. */
endpoint: string;
/** Auth + extra headers for the export request. */
headers: Record<string, string>;
/** Service name (also the resource `service.name`). */
serviceName: string;
/** OTel resource attributes (must include `project_name` / `project_type`). */
resourceAttributes: Record<string, string>;
/** Export request timeout in ms. */
timeout?: number;
/** Spans per export batch. */
batchSize?: number;
}

/**
* Mastra span type → Future AGI span kind (`gen_ai.span.kind`).
*
* The FI backend maps these (case-insensitively) to its observation types
* (llm / agent / tool / chain / ...). Mastra emits `gen_ai.operation.name` but
* NOT a span kind, so without this mapping every span renders as "unknown".
*/
const SPAN_KIND_BY_TYPE: Partial<Record<SpanType, FISpanKind>> = {
[SpanType.AGENT_RUN]: FISpanKind.AGENT,
[SpanType.MODEL_GENERATION]: FISpanKind.LLM,
[SpanType.MODEL_INFERENCE]: FISpanKind.LLM,
[SpanType.MODEL_STEP]: FISpanKind.CHAIN,
[SpanType.TOOL_CALL]: FISpanKind.TOOL,
[SpanType.MCP_TOOL_CALL]: FISpanKind.TOOL,
[SpanType.CLIENT_TOOL_CALL]: FISpanKind.TOOL,
[SpanType.WORKFLOW_RUN]: FISpanKind.CHAIN,
[SpanType.WORKFLOW_STEP]: FISpanKind.CHAIN,
[SpanType.WORKFLOW_CONDITIONAL]: FISpanKind.CHAIN,
[SpanType.WORKFLOW_PARALLEL]: FISpanKind.CHAIN,
[SpanType.WORKFLOW_LOOP]: FISpanKind.CHAIN,
[SpanType.GENERIC]: FISpanKind.CHAIN,
[SpanType.RAG_EMBEDDING]: FISpanKind.EMBEDDING,
[SpanType.RAG_VECTOR_OPERATION]: FISpanKind.RETRIEVER,
};

/**
* Write `<key>.value` + `<key>.mime_type` the way the FI backend's `set_io_value`
* expects, so the trace UI renders the input/output.
*/
function setIoValue(
attrs: Record<string, unknown>,
key: "input" | "output",
value: unknown,
): void {
if (value === undefined || value === null) return;
if (typeof value === "object") {
attrs[`${key}.value`] = JSON.stringify(value);
attrs[`${key}.mime_type`] = "application/json";
} else {
attrs[`${key}.value`] = String(value);
attrs[`${key}.mime_type`] = "text/plain";
}
}

/**
* Enrich a converted OTLP span in place with the attributes Future AGI keys on:
* `gen_ai.span.kind` (from the Mastra span type) and `input.value`/`output.value`
* (from the span's input/output). Existing values are not overwritten. Exported
* for unit testing.
*/
export function enrichSpan(otelSpan: ReadableSpan, span: AnyExportedSpan): void {
const attrs = otelSpan.attributes as Record<string, unknown>;

// Span kind so the span isn't typed "unknown" in Future AGI.
const kind = SPAN_KIND_BY_TYPE[span.type];
if (kind && attrs["gen_ai.span.kind"] == null) {
attrs["gen_ai.span.kind"] = kind;
}

// input.value / output.value so prompt / response / tool I/O renders.
// (SpanConverter puts these under gen_ai.*/mastra.* keys the backend doesn't
// surface in the I/O preview.)
const s = span as { input?: unknown; output?: unknown };
if (attrs["input.value"] == null) setIoValue(attrs, "input", s.input);
if (attrs["output.value"] == null) setIoValue(attrs, "output", s.output);
}

/**
* Mastra v1 observability exporter for Future AGI.
*
* Reuses `@mastra/otel-exporter`'s {@link SpanConverter} to turn Mastra spans
* into OTLP spans (standard `gen_ai.*` conventions), then enriches each span with
* the attributes Future AGI keys on for display (`gen_ai.span.kind`,
* `input.value` / `output.value`), and ships OTLP http/protobuf to the collector.
*/
export class FIMastraSpanExporter extends BaseExporter {
name = "future-agi";
private readonly converter: SpanConverter;
private readonly processor: BatchSpanProcessor;

constructor(config: FISpanExporterConfig) {
super();
this.converter = new SpanConverter({
format: "GenAI_v1_38_0",
packageName: "@traceai/mastra",
serviceName: config.serviceName,
// Only `resourceAttributes` is read off this config by the converter.
config: { resourceAttributes: config.resourceAttributes } as any,
});
const exporter = new OTLPTraceExporter({
url: config.endpoint,
headers: config.headers,
...(config.timeout !== undefined ? { timeoutMillis: config.timeout } : {}),
});
this.processor = new BatchSpanProcessor(
exporter,
config.batchSize !== undefined
? { maxExportBatchSize: config.batchSize }
: undefined,
);
}

protected async _exportTracingEvent(event: TracingEvent): Promise<void> {
if (event.type !== "span_ended") return;
const span = event.exportedSpan;
try {
const otelSpan = await this.converter.convertSpan(span);
enrichSpan(otelSpan, span);
this.processor.onEnd(otelSpan);
} catch (error) {
this.logger.error(
`[@traceai/mastra] Failed to export span ${span.id}`,
error as Error,
);
}
}

async flush(): Promise<void> {
await this.processor.forceFlush();
}

async shutdown(): Promise<void> {
await this.processor.shutdown();
}
}
158 changes: 158 additions & 0 deletions typescript/packages/traceai_mastra/src/FIObservability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { Observability } from "@mastra/observability";
import { SpanType } from "@mastra/core/observability";
import {
FIMastraSpanExporter,
type FISpanExporterConfig,
} from "./FIMastraSpanExporter.js";


const DEFAULT_FI_BASE_URL = "https://api.futureagi.com";
const FI_TRACES_PATH = "/tracer/v1/traces";

export interface FIMastraExporterOptions {
/** Service name (resource `service.name`). Defaults to `"mastra-app"`. */
serviceName?: string;
/** Future AGI API key. Defaults to `process.env.FI_API_KEY`. */
apiKey?: string;
/** Future AGI secret key. Defaults to `process.env.FI_SECRET_KEY`. */
secretKey?: string;
/**
* Full traces endpoint URL. When set, overrides `baseUrl`.
* Defaults to `${baseUrl}/tracer/v1/traces`.
*/
endpoint?: string;
/**
* Base collector URL. The `/tracer/v1/traces` path is appended.
* Defaults to `process.env.FI_BASE_URL` or `https://api.futureagi.com`.
*/
baseUrl?: string;
/** Extra headers merged into the export request. */
headers?: Record<string, string>;
/**
* Future AGI project name. This is the `project_name` resource attribute the
* Future AGI collector keys on — WITHOUT it, traces are ingested but no project
* is created and nothing shows in the dashboard. Defaults to `FI_PROJECT_NAME`
* env, then (via {@link createFIObservability}) the `serviceName`.
*/
projectName?: string;
/**
* Future AGI project type. `"observe"` for continuous tracing (default),
* `"experiment"` for evaluation runs with project versions.
*/
projectType?: "observe" | "experiment";
/** Extra OTel resource attributes merged onto every span's resource. */
resourceAttributes?: Record<string, string>;
/** Export request timeout in ms. */
timeout?: number;
/** Spans per export batch. */
batchSize?: number;
}

/** @internal Exported for unit testing; not part of the public package API. */
export function resolveResourceAttributes(
options: FIMastraExporterOptions,
): Record<string, string> {
const projectName = options.projectName ?? process.env.FI_PROJECT_NAME;
return {
// Future AGI keys the project on these resource attributes.
...(projectName ? { project_name: projectName } : {}),
project_type: options.projectType ?? "observe",
...(options.resourceAttributes ?? {}),
};
}

/** @internal Exported for unit testing; not part of the public package API. */
export function resolveEndpoint(options: FIMastraExporterOptions): string {
if (options.endpoint) return options.endpoint;
const base = options.baseUrl ?? process.env.FI_BASE_URL ?? DEFAULT_FI_BASE_URL;
return base.replace(/\/+$/, "") + FI_TRACES_PATH;
}

/** @internal Exported for unit testing; not part of the public package API. */
export function resolveAuth(options: FIMastraExporterOptions): {
apiKey: string;
secretKey: string;
} {
const apiKey = options.apiKey ?? process.env.FI_API_KEY;
const secretKey = options.secretKey ?? process.env.FI_SECRET_KEY;
if (!apiKey || !secretKey) {
throw new Error(
"[@traceai/mastra] Missing Future AGI credentials. Set FI_API_KEY and " +
"FI_SECRET_KEY environment variables, or pass { apiKey, secretKey } to " +
"createFIMastraExporter()/createFIObservability().",
);
}
return { apiKey, secretKey };
}

/**
* Create a Mastra v1 observability exporter pre-configured for Future AGI.
*
*/
export function createFIMastraExporter(
options: FIMastraExporterOptions = {},
): FIMastraSpanExporter {
const { apiKey, secretKey } = resolveAuth(options);
const config: FISpanExporterConfig = {
endpoint: resolveEndpoint(options),
headers: {
"x-api-key": apiKey,
"x-secret-key": secretKey,
...(options.headers ?? {}),
},
serviceName: options.serviceName ?? "mastra-app",
resourceAttributes: resolveResourceAttributes(options),
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
...(options.batchSize !== undefined ? { batchSize: options.batchSize } : {}),
};
return new FIMastraSpanExporter(config);
}

export interface FIObservabilityOptions extends FIMastraExporterOptions {
/**
* Mastra span types to drop before export. Defaults to `[SpanType.MODEL_CHUNK]`
* — per-chunk streaming spans that are pure noise in an observability backend.
* Pass `[]` to export everything.
*/
excludeSpanTypes?: SpanType[];
}

/**
* Create a ready-to-use Mastra v1 {@link Observability} instance wired to Future AGI.
*
* @example
* ```ts
* import { Mastra } from "@mastra/core";
* import { createFIObservability } from "@traceai/mastra";
*
* export const mastra = new Mastra({
* agents: { ... },
* observability: createFIObservability({ serviceName: "my-app" }),
* });
* ```
*/
export function createFIObservability(
options: FIObservabilityOptions = {},
): Observability {
const {
serviceName = "mastra-app",
excludeSpanTypes = [SpanType.MODEL_CHUNK],
...exporterOptions
} = options;
return new Observability({
configs: {
otel: {
serviceName,
excludeSpanTypes,
exporters: [
createFIMastraExporter({
...exporterOptions,
serviceName,
// Default the FI project to the service name so a project is created.
projectName: exporterOptions.projectName ?? serviceName,
}),
],
},
},
});
}
5 changes: 5 additions & 0 deletions typescript/packages/traceai_mastra/src/FITraceExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ type ConstructorArgs = {
spanFilter?: (span: ReadableSpanFromExporter) => boolean;
} & NonNullable<ConstructorParameters<typeof OTLPTraceExporter>[0]>;

/**
* @deprecated Mastra v0.x only. Mastra v1 (>= 1.16) removed the `telemetry`
* config key this exporter plugs into, so it no longer receives spans. For
* Mastra v1 use {@link createFIObservability} / {@link createFIMastraExporter}.
*/
export class FITraceExporter extends OTLPTraceExporter {
private readonly spanFilter?: (span: ReadableSpanFromExporter) => boolean;

Expand Down
Loading