@nightowlsdev/telemetry-langfuse 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Night Owls contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @nightowlsdev/telemetry-langfuse
2
+
3
+ A **Langfuse v5** telemetry exporter for `@nightowlsdev/core`. It maps a swarm run's
4
+ `run`/`generation`/`tool` `SwarmSpan`s to OpenTelemetry spans (via
5
+ `@nightowlsdev/telemetry-core`) and forwards them through Langfuse's OTel-native
6
+ `LangfuseSpanProcessor`. Langfuse's default filter exports any span carrying `gen_ai.*`
7
+ attributes — which `@nightowlsdev/telemetry-core` emits — so each model call lands as a Langfuse
8
+ **generation** with its model, token usage, and cost.
9
+
10
+ > Langfuse v5 is OTel-native. The old v3 `langfuse.trace()` / `.generation()` API is gone;
11
+ > this package targets `@langfuse/otel`'s `LangfuseSpanProcessor`.
12
+
13
+ It never imports `@mastra/*` and re-exports no Mastra types (the engine wall, CONTRACTS §1).
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @nightowlsdev/telemetry-langfuse @nightowlsdev/core
19
+ ```
20
+
21
+ ## Factory
22
+
23
+ ```ts
24
+ import { langfuseTelemetry } from "@nightowlsdev/telemetry-langfuse";
25
+
26
+ const telemetry = langfuseTelemetry({
27
+ publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
28
+ secretKey: process.env.LANGFUSE_SECRET_KEY!,
29
+ baseUrl: "https://cloud.langfuse.com", // optional
30
+ exportMode: "immediate", // default here — flushes per run (serverless-safe)
31
+ environment: "prod", // optional
32
+ release: "1.4.0", // optional
33
+ });
34
+ ```
35
+
36
+ `exportMode: "immediate"` (the default) flushes on each run's export — ideal for
37
+ serverless. Use `"batched"` for long-running hosts. The replayer **awaits `forceFlush()`**
38
+ so spans aren't lost.
39
+
40
+ ## Wiring into a swarm
41
+
42
+ ```ts
43
+ import { defineSwarm } from "@nightowlsdev/core";
44
+ import { langfuseTelemetry } from "@nightowlsdev/telemetry-langfuse";
45
+
46
+ const swarm = defineSwarm({
47
+ storage,
48
+ agents,
49
+ models: { allow: ["openai/gpt-5.5"] },
50
+ modelFactory,
51
+ cost: { maxSteps: 8, maxCostUsd: 1 },
52
+ telemetry: langfuseTelemetry({ publicKey, secretKey }),
53
+ });
54
+ ```
55
+
56
+ Telemetry is **best-effort**: a throwing or hung exporter never breaks the run (the engine
57
+ exports in a `finally` and swallows export errors). Pass an array of exporters (or
58
+ `compositeTelemetry`) to send to Langfuse and an OTLP backend at once.
59
+
60
+ ## Engine wall
61
+
62
+ The built `dist/index.d.ts` contains zero `@mastra` references (enforced by
63
+ `test/wall.test.ts`).
package/dist/index.cjs ADDED
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ langfuseTelemetry: () => langfuseTelemetry,
24
+ nightOwlsPlugin: () => nightOwlsPlugin
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var import_otel = require("@langfuse/otel");
28
+ var import_telemetry_core = require("@nightowlsdev/telemetry-core");
29
+
30
+ // src/plugin.ts
31
+ var nightOwlsPlugin = {
32
+ name: "telemetry-langfuse",
33
+ version: "0.0.0",
34
+ kind: "telemetry",
35
+ pkg: "@nightowlsdev/telemetry-langfuse",
36
+ description: "Langfuse (v5, OTel-native) exporter \u2014 LLM generations with model + token usage + cost.",
37
+ env: [
38
+ { key: "LANGFUSE_PUBLIC_KEY", example: "pk-lf-...", comment: "Langfuse public key" },
39
+ { key: "LANGFUSE_SECRET_KEY", example: "sk-lf-...", comment: "Langfuse secret key (server-side ONLY)" },
40
+ {
41
+ key: "LANGFUSE_BASEURL",
42
+ example: "https://cloud.langfuse.com",
43
+ comment: "Langfuse host (EU/US cloud or your self-hosted URL)"
44
+ }
45
+ ],
46
+ config: {
47
+ import: "import { langfuseTelemetry } from '@nightowlsdev/telemetry-langfuse';",
48
+ snippet: "telemetry = [...(telemetry ?? []), langfuseTelemetry({ publicKey: env.LANGFUSE_PUBLIC_KEY, secretKey: env.LANGFUSE_SECRET_KEY, baseUrl: env.LANGFUSE_BASEURL })];",
49
+ marker: "telemetry"
50
+ },
51
+ // Print-only (idempotent) — runs on init/install + `owl init telemetry-langfuse`. NEVER connects.
52
+ init: (ctx) => {
53
+ ctx.log(
54
+ "Langfuse telemetry wired (composes with any other telemetry adapter) \u2014 set LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY + LANGFUSE_BASEURL."
55
+ );
56
+ },
57
+ commands: [
58
+ {
59
+ name: "info",
60
+ description: "Show the env + config this telemetry adapter contributes.",
61
+ // PURE — no network. Reads only the static manifest data.
62
+ run: (ctx) => {
63
+ ctx.log("env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASEURL");
64
+ ctx.log("config: telemetry = [...(telemetry ?? []), langfuseTelemetry({ publicKey, secretKey, baseUrl })]");
65
+ }
66
+ }
67
+ ]
68
+ };
69
+
70
+ // src/index.ts
71
+ function langfuseTelemetry(opts = {}) {
72
+ const processor = opts.processor ?? new import_otel.LangfuseSpanProcessor({
73
+ publicKey: opts.publicKey,
74
+ secretKey: opts.secretKey,
75
+ baseUrl: opts.baseUrl,
76
+ exportMode: opts.exportMode ?? "immediate",
77
+ environment: opts.environment,
78
+ release: opts.release
79
+ });
80
+ return (0, import_telemetry_core.replayerExporter)({ processor, serviceName: "nightowls" });
81
+ }
82
+ // Annotate the CommonJS export names for ESM import in node:
83
+ 0 && (module.exports = {
84
+ langfuseTelemetry,
85
+ nightOwlsPlugin
86
+ });
@@ -0,0 +1,78 @@
1
+ import { SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+ import { TelemetryExporter } from '@nightowlsdev/core';
3
+
4
+ /**
5
+ * The Night Owls adapter plugin manifest for the Langfuse exporter. `@nightowlsdev/cli` discovers this,
6
+ * dynamic-imports it, and acts on it declaratively:
7
+ * - `env` → merged into `.env.example` (idempotent, only if the KEY is absent),
8
+ * - `config` → the `import` + wiring `snippet` inserted at the `// nightowls:telemetry` marker. Telemetry
9
+ * COMPOSES: the snippet pushes this exporter onto the `telemetry` array, so multiple
10
+ * telemetry adapters coexist (`telemetry = [...(telemetry ?? []), langfuseTelemetry({...})];`),
11
+ * - `init` → a print-only hook run after the wiring (it NEVER connects to Langfuse),
12
+ * - `commands`→ `owl telemetry-langfuse <cmd>` subcommands (e.g. `info` — pure, no network).
13
+ *
14
+ * This object STRUCTURALLY matches the CLI's `NightOwlsPlugin` type but does NOT import it (no dependency
15
+ * cycle): all codegen lives in `@nightowlsdev/cli`; this adapter stays data-only. The `init`/command handler
16
+ * contexts are typed STRUCTURALLY (local `Ctx`/inline shapes) — never importing `@nightowlsdev/cli`.
17
+ */
18
+ /** Structural context for this adapter's pure command handlers — matches the CLI's PluginCommandContext. */
19
+ type Ctx = {
20
+ cwd: string;
21
+ log: (m: string) => void;
22
+ args: string[];
23
+ options: Record<string, unknown>;
24
+ };
25
+ declare const nightOwlsPlugin: {
26
+ readonly name: "telemetry-langfuse";
27
+ readonly version: "0.0.0";
28
+ readonly kind: "telemetry";
29
+ readonly pkg: "@nightowlsdev/telemetry-langfuse";
30
+ readonly description: "Langfuse (v5, OTel-native) exporter — LLM generations with model + token usage + cost.";
31
+ readonly env: readonly [{
32
+ readonly key: "LANGFUSE_PUBLIC_KEY";
33
+ readonly example: "pk-lf-...";
34
+ readonly comment: "Langfuse public key";
35
+ }, {
36
+ readonly key: "LANGFUSE_SECRET_KEY";
37
+ readonly example: "sk-lf-...";
38
+ readonly comment: "Langfuse secret key (server-side ONLY)";
39
+ }, {
40
+ readonly key: "LANGFUSE_BASEURL";
41
+ readonly example: "https://cloud.langfuse.com";
42
+ readonly comment: "Langfuse host (EU/US cloud or your self-hosted URL)";
43
+ }];
44
+ readonly config: {
45
+ readonly import: "import { langfuseTelemetry } from '@nightowlsdev/telemetry-langfuse';";
46
+ readonly snippet: "telemetry = [...(telemetry ?? []), langfuseTelemetry({ publicKey: env.LANGFUSE_PUBLIC_KEY, secretKey: env.LANGFUSE_SECRET_KEY, baseUrl: env.LANGFUSE_BASEURL })];";
47
+ readonly marker: "telemetry";
48
+ };
49
+ readonly init: (ctx: {
50
+ cwd: string;
51
+ log: (m: string) => void;
52
+ }) => void;
53
+ readonly commands: readonly [{
54
+ readonly name: "info";
55
+ readonly description: "Show the env + config this telemetry adapter contributes.";
56
+ readonly run: (ctx: Ctx) => void;
57
+ }];
58
+ };
59
+
60
+ interface LangfuseTelemetryOpts {
61
+ publicKey?: string;
62
+ secretKey?: string;
63
+ baseUrl?: string;
64
+ /** 'immediate' (serverless, default here) flushes per export; 'batched' for long-running hosts. */
65
+ exportMode?: "immediate" | "batched";
66
+ environment?: string;
67
+ release?: string;
68
+ /** Test/advanced override: provide the SpanProcessor directly (skips LangfuseSpanProcessor construction). */
69
+ processor?: SpanProcessor;
70
+ }
71
+ /**
72
+ * Map Night Owls SwarmSpans to Langfuse (v5, OTel-native). The LangfuseSpanProcessor's default filter
73
+ * forwards any span carrying gen_ai.* attributes — which telemetry-core emits — so generations land
74
+ * as Langfuse generations with model + token usage + cost.
75
+ */
76
+ declare function langfuseTelemetry(opts?: LangfuseTelemetryOpts): TelemetryExporter;
77
+
78
+ export { type LangfuseTelemetryOpts, langfuseTelemetry, nightOwlsPlugin };
@@ -0,0 +1,78 @@
1
+ import { SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+ import { TelemetryExporter } from '@nightowlsdev/core';
3
+
4
+ /**
5
+ * The Night Owls adapter plugin manifest for the Langfuse exporter. `@nightowlsdev/cli` discovers this,
6
+ * dynamic-imports it, and acts on it declaratively:
7
+ * - `env` → merged into `.env.example` (idempotent, only if the KEY is absent),
8
+ * - `config` → the `import` + wiring `snippet` inserted at the `// nightowls:telemetry` marker. Telemetry
9
+ * COMPOSES: the snippet pushes this exporter onto the `telemetry` array, so multiple
10
+ * telemetry adapters coexist (`telemetry = [...(telemetry ?? []), langfuseTelemetry({...})];`),
11
+ * - `init` → a print-only hook run after the wiring (it NEVER connects to Langfuse),
12
+ * - `commands`→ `owl telemetry-langfuse <cmd>` subcommands (e.g. `info` — pure, no network).
13
+ *
14
+ * This object STRUCTURALLY matches the CLI's `NightOwlsPlugin` type but does NOT import it (no dependency
15
+ * cycle): all codegen lives in `@nightowlsdev/cli`; this adapter stays data-only. The `init`/command handler
16
+ * contexts are typed STRUCTURALLY (local `Ctx`/inline shapes) — never importing `@nightowlsdev/cli`.
17
+ */
18
+ /** Structural context for this adapter's pure command handlers — matches the CLI's PluginCommandContext. */
19
+ type Ctx = {
20
+ cwd: string;
21
+ log: (m: string) => void;
22
+ args: string[];
23
+ options: Record<string, unknown>;
24
+ };
25
+ declare const nightOwlsPlugin: {
26
+ readonly name: "telemetry-langfuse";
27
+ readonly version: "0.0.0";
28
+ readonly kind: "telemetry";
29
+ readonly pkg: "@nightowlsdev/telemetry-langfuse";
30
+ readonly description: "Langfuse (v5, OTel-native) exporter — LLM generations with model + token usage + cost.";
31
+ readonly env: readonly [{
32
+ readonly key: "LANGFUSE_PUBLIC_KEY";
33
+ readonly example: "pk-lf-...";
34
+ readonly comment: "Langfuse public key";
35
+ }, {
36
+ readonly key: "LANGFUSE_SECRET_KEY";
37
+ readonly example: "sk-lf-...";
38
+ readonly comment: "Langfuse secret key (server-side ONLY)";
39
+ }, {
40
+ readonly key: "LANGFUSE_BASEURL";
41
+ readonly example: "https://cloud.langfuse.com";
42
+ readonly comment: "Langfuse host (EU/US cloud or your self-hosted URL)";
43
+ }];
44
+ readonly config: {
45
+ readonly import: "import { langfuseTelemetry } from '@nightowlsdev/telemetry-langfuse';";
46
+ readonly snippet: "telemetry = [...(telemetry ?? []), langfuseTelemetry({ publicKey: env.LANGFUSE_PUBLIC_KEY, secretKey: env.LANGFUSE_SECRET_KEY, baseUrl: env.LANGFUSE_BASEURL })];";
47
+ readonly marker: "telemetry";
48
+ };
49
+ readonly init: (ctx: {
50
+ cwd: string;
51
+ log: (m: string) => void;
52
+ }) => void;
53
+ readonly commands: readonly [{
54
+ readonly name: "info";
55
+ readonly description: "Show the env + config this telemetry adapter contributes.";
56
+ readonly run: (ctx: Ctx) => void;
57
+ }];
58
+ };
59
+
60
+ interface LangfuseTelemetryOpts {
61
+ publicKey?: string;
62
+ secretKey?: string;
63
+ baseUrl?: string;
64
+ /** 'immediate' (serverless, default here) flushes per export; 'batched' for long-running hosts. */
65
+ exportMode?: "immediate" | "batched";
66
+ environment?: string;
67
+ release?: string;
68
+ /** Test/advanced override: provide the SpanProcessor directly (skips LangfuseSpanProcessor construction). */
69
+ processor?: SpanProcessor;
70
+ }
71
+ /**
72
+ * Map Night Owls SwarmSpans to Langfuse (v5, OTel-native). The LangfuseSpanProcessor's default filter
73
+ * forwards any span carrying gen_ai.* attributes — which telemetry-core emits — so generations land
74
+ * as Langfuse generations with model + token usage + cost.
75
+ */
76
+ declare function langfuseTelemetry(opts?: LangfuseTelemetryOpts): TelemetryExporter;
77
+
78
+ export { type LangfuseTelemetryOpts, langfuseTelemetry, nightOwlsPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ // src/index.ts
2
+ import { LangfuseSpanProcessor } from "@langfuse/otel";
3
+ import { replayerExporter } from "@nightowlsdev/telemetry-core";
4
+
5
+ // src/plugin.ts
6
+ var nightOwlsPlugin = {
7
+ name: "telemetry-langfuse",
8
+ version: "0.0.0",
9
+ kind: "telemetry",
10
+ pkg: "@nightowlsdev/telemetry-langfuse",
11
+ description: "Langfuse (v5, OTel-native) exporter \u2014 LLM generations with model + token usage + cost.",
12
+ env: [
13
+ { key: "LANGFUSE_PUBLIC_KEY", example: "pk-lf-...", comment: "Langfuse public key" },
14
+ { key: "LANGFUSE_SECRET_KEY", example: "sk-lf-...", comment: "Langfuse secret key (server-side ONLY)" },
15
+ {
16
+ key: "LANGFUSE_BASEURL",
17
+ example: "https://cloud.langfuse.com",
18
+ comment: "Langfuse host (EU/US cloud or your self-hosted URL)"
19
+ }
20
+ ],
21
+ config: {
22
+ import: "import { langfuseTelemetry } from '@nightowlsdev/telemetry-langfuse';",
23
+ snippet: "telemetry = [...(telemetry ?? []), langfuseTelemetry({ publicKey: env.LANGFUSE_PUBLIC_KEY, secretKey: env.LANGFUSE_SECRET_KEY, baseUrl: env.LANGFUSE_BASEURL })];",
24
+ marker: "telemetry"
25
+ },
26
+ // Print-only (idempotent) — runs on init/install + `owl init telemetry-langfuse`. NEVER connects.
27
+ init: (ctx) => {
28
+ ctx.log(
29
+ "Langfuse telemetry wired (composes with any other telemetry adapter) \u2014 set LANGFUSE_PUBLIC_KEY + LANGFUSE_SECRET_KEY + LANGFUSE_BASEURL."
30
+ );
31
+ },
32
+ commands: [
33
+ {
34
+ name: "info",
35
+ description: "Show the env + config this telemetry adapter contributes.",
36
+ // PURE — no network. Reads only the static manifest data.
37
+ run: (ctx) => {
38
+ ctx.log("env: LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASEURL");
39
+ ctx.log("config: telemetry = [...(telemetry ?? []), langfuseTelemetry({ publicKey, secretKey, baseUrl })]");
40
+ }
41
+ }
42
+ ]
43
+ };
44
+
45
+ // src/index.ts
46
+ function langfuseTelemetry(opts = {}) {
47
+ const processor = opts.processor ?? new LangfuseSpanProcessor({
48
+ publicKey: opts.publicKey,
49
+ secretKey: opts.secretKey,
50
+ baseUrl: opts.baseUrl,
51
+ exportMode: opts.exportMode ?? "immediate",
52
+ environment: opts.environment,
53
+ release: opts.release
54
+ });
55
+ return replayerExporter({ processor, serviceName: "nightowls" });
56
+ }
57
+ export {
58
+ langfuseTelemetry,
59
+ nightOwlsPlugin
60
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@nightowlsdev/telemetry-langfuse",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/cueplusplus/corale.git",
12
+ "directory": "packages/telemetry-langfuse"
13
+ },
14
+ "homepage": "https://github.com/cueplusplus/corale#readme",
15
+ "sideEffects": false,
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "@langfuse/otel": "^5.4.1",
31
+ "@opentelemetry/sdk-trace-base": "^2.0.1",
32
+ "@nightowlsdev/telemetry-core": "0.1.2"
33
+ },
34
+ "peerDependencies": {
35
+ "@nightowlsdev/core": "0.3.0"
36
+ },
37
+ "devDependencies": {
38
+ "@opentelemetry/api": "^1.9.0",
39
+ "@types/node": "^24.12.4",
40
+ "tsup": "8.5.1",
41
+ "typescript": "6.0.3",
42
+ "vitest": "^3.2.0",
43
+ "@nightowlsdev/eslint-config": "0.0.0",
44
+ "@nightowlsdev/tsconfig": "0.0.0",
45
+ "@nightowlsdev/core": "0.3.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "lint": "eslint src"
52
+ }
53
+ }