├── examples
├── browser
│ ├── README.md
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── proxy.ts
│ │ └── main.ts
│ ├── .gitignore
│ ├── index.html
│ ├── tsconfig.json
│ └── package.json
├── node
│ ├── .env
│ ├── tsconfig.json
│ ├── .env.example
│ ├── package.json
│ └── index.ts
├── nextjs
│ ├── app
│ │ ├── favicon.ico
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── instrumentation.ts
│ ├── next.config.ts
│ ├── .codesandbox
│ │ └── tasks.json
│ ├── middleware.ts
│ ├── package.json
│ ├── .gitignore
│ └── tsconfig.json
├── cf-tail-worker
│ ├── .vscode
│ │ └── settings.json
│ ├── .prettierrc
│ ├── .editorconfig
│ ├── src
│ │ └── index.ts
│ ├── worker-configuration.d.ts
│ ├── package.json
│ ├── wrangler.jsonc
│ ├── tsconfig.json
│ └── .gitignore
├── deno-project
│ ├── main.ts
│ ├── deno.json
│ └── start.sh
├── cf-worker
│ ├── .prettierrc
│ ├── README.md
│ ├── worker-configuration.d.ts
│ ├── .editorconfig
│ ├── vitest.config.mts
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── wrangler.jsonc
│ ├── tsconfig.json
│ └── .gitignore
├── cf-producer-worker
│ ├── .prettierrc
│ ├── worker-configuration.d.ts
│ ├── .editorconfig
│ ├── vitest.config.mts
│ ├── wrangler.jsonc
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ └── .gitignore
├── express
│ ├── tsconfig.json
│ ├── instrumentation.ts
│ ├── .env.example
│ ├── app.ts
│ └── package.json
└── nextjs-client-side-instrumentation
│ ├── app
│ ├── favicon.ico
│ ├── api
│ │ └── hello
│ │ │ └── route.ts
│ ├── page.tsx
│ ├── globals.css
│ ├── layout.tsx
│ ├── components
│ │ ├── HelloButton.tsx
│ │ └── ClientInstrumentationProvider.tsx
│ └── page.module.css
│ ├── instrumentation.ts
│ ├── public
│ ├── vercel.svg
│ ├── window.svg
│ ├── file.svg
│ ├── globe.svg
│ └── next.svg
│ ├── eslint.config.mjs
│ ├── next.config.ts
│ ├── .gitignore
│ ├── middleware.ts
│ ├── tsconfig.json
│ ├── README.md
│ └── package.json
├── .gitignore
├── packages
├── logfire-api
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── logfireApiConfig.test.ts
│ │ ├── constants.ts
│ │ ├── ULIDGenerator.ts
│ │ ├── index.test.ts
│ │ ├── serializeAttributes.ts
│ │ ├── logfireApiConfig.ts
│ │ ├── AttributeScrubber.ts
│ │ ├── index.ts
│ │ └── formatter.ts
│ ├── prettier.config.mjs
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── eslint.config.mjs
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ └── CHANGELOG.md
├── logfire-cf-workers
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── exportTailEventsToLogfire.ts
│ │ ├── TailWorkerExporter.ts
│ │ ├── LogfireCloudflareConsoleSpanExporter.ts
│ │ ├── OtlpTransformerTypes.ts
│ │ └── index.ts
│ ├── prettier.config.mjs
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── eslint.config.mjs
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ └── CHANGELOG.md
├── logfire-node
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── VoidTraceExporter.ts
│ │ ├── VoidMetricExporter.ts
│ │ ├── utils.ts
│ │ ├── metricExporter.ts
│ │ ├── index.ts
│ │ ├── LogfireConsoleSpanExporter.ts
│ │ ├── traceExporter.ts
│ │ ├── sdk.ts
│ │ └── logfireConfig.ts
│ ├── prettier.config.mjs
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── eslint.config.mjs
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ └── CHANGELOG.md
├── logfire-browser
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── OTLPTraceExporterWithDynamicHeaders.ts
│ │ ├── LogfireSpanProcessor.ts
│ │ └── index.ts
│ ├── prettier.config.mjs
│ ├── vite.config.ts
│ ├── eslint.config.mjs
│ ├── tsconfig.json
│ ├── .gitignore
│ ├── README.md
│ ├── CHANGELOG.md
│ └── package.json
└── tooling-config
│ ├── prettier-config.d.mts
│ ├── prettier-config.mjs
│ ├── vite-config.d.mts
│ ├── eslint-config.d.mts
│ ├── tsconfig.base.json
│ ├── package.json
│ ├── eslint-config.mjs
│ └── vite-config.mjs
├── .changeset
├── config.json
└── README.md
├── .claude
└── settings.local.json
├── CONTRIBUTING.md
├── agent
└── prompts
│ └── add-default-export.md
├── turbo.json
├── package.json
├── LICENSE
├── .github
└── workflows
│ └── main.yml
└── CLAUDE.md
/examples/browser/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/browser/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | packages/*/*.tgz
3 | .turbo
4 | .env
5 | scratch/
6 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/node/.env:
--------------------------------------------------------------------------------
1 | LOGFIRE_BASE_URL=http://localhost:8000
2 | LOGFIRE_TOKEN=test-e2e-write-token
3 |
--------------------------------------------------------------------------------
/examples/nextjs/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pydantic/logfire-js/HEAD/examples/nextjs/app/favicon.ico
--------------------------------------------------------------------------------
/examples/cf-tail-worker/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "wrangler.json": "jsonc"
4 | }
5 | }
--------------------------------------------------------------------------------
/packages/logfire-node/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare const PACKAGE_VERSION: string
3 |
--------------------------------------------------------------------------------
/examples/deno-project/main.ts:
--------------------------------------------------------------------------------
1 | Deno.serve({ port: 4242 }, (_req) => {
2 | return new Response("Hello, World!");
3 | });
4 |
--------------------------------------------------------------------------------
/packages/logfire-browser/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare const PACKAGE_VERSION: string
3 |
--------------------------------------------------------------------------------
/examples/cf-worker/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/packages/logfire-api/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/prettier-config'
2 |
3 | export default baseConfig
4 |
--------------------------------------------------------------------------------
/packages/tooling-config/prettier-config.d.mts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'prettier'
2 |
3 | declare const config: Config
4 | export default config
5 |
--------------------------------------------------------------------------------
/packages/logfire-browser/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/prettier-config'
2 |
3 | export default baseConfig
4 |
--------------------------------------------------------------------------------
/packages/logfire-node/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/prettier-config'
2 |
3 | export default baseConfig
4 |
--------------------------------------------------------------------------------
/examples/express/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node20/tsconfig.json",
3 | "compilerOptions": {
4 | "moduleDetection": "force"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/prettier-config'
2 |
3 | export default baseConfig
4 |
--------------------------------------------------------------------------------
/packages/tooling-config/prettier-config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | printWidth: 140,
3 | semi: false,
4 | singleQuote: true,
5 | trailingComma: "es5",
6 | };
7 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pydantic/logfire-js/HEAD/examples/nextjs-client-side-instrumentation/app/favicon.ico
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/instrumentation.ts:
--------------------------------------------------------------------------------
1 | import { registerOTel } from "@vercel/otel";
2 |
3 | export function register() {
4 | registerOTel();
5 | }
6 |
--------------------------------------------------------------------------------
/packages/tooling-config/vite-config.d.mts:
--------------------------------------------------------------------------------
1 | import type { UserConfig } from 'vite'
2 |
3 | export default function defineConfig(entry: string, external?: string[]): UserConfig
4 |
--------------------------------------------------------------------------------
/examples/deno-project/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "dev": "deno run --allow-net main.ts"
4 | },
5 | "imports": {
6 | "@std/assert": "jsr:@std/assert@1"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/tooling-config/eslint-config.d.mts:
--------------------------------------------------------------------------------
1 | import tseslint from 'typescript-eslint'
2 |
3 | declare const defaultExport: ReturnType
4 | export default defaultExport
5 |
--------------------------------------------------------------------------------
/examples/cf-worker/README.md:
--------------------------------------------------------------------------------
1 | # Cloudflare worker instrumentation example
2 |
3 | An example that shows how to use the `@pydantic/logfire-cf-workers` package to
4 | instrument a Cloudflare worker.
5 |
--------------------------------------------------------------------------------
/examples/cf-worker/worker-configuration.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by Wrangler
2 | // After adding bindings to `wrangler.jsonc`, regenerate this interface via `npm run cf-typegen`
3 | interface Env {
4 | }
5 |
--------------------------------------------------------------------------------
/examples/express/instrumentation.ts:
--------------------------------------------------------------------------------
1 | import * as logfire from '@pydantic/logfire-node'
2 | import 'dotenv/config'
3 |
4 | logfire.configure({
5 | diagLogLevel: logfire.DiagLogLevel.ERROR,
6 | })
7 |
--------------------------------------------------------------------------------
/examples/nextjs/instrumentation.ts:
--------------------------------------------------------------------------------
1 | import { registerOTel } from '@vercel/otel';
2 |
3 | export function register() {
4 | registerOTel({
5 | serviceName: 'vercel-loves-logfire',
6 | })
7 | }
8 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/worker-configuration.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by Wrangler
2 | // After adding bindings to `wrangler.jsonc`, regenerate this interface via `npm run cf-typegen`
3 | interface Env {
4 | }
5 |
--------------------------------------------------------------------------------
/examples/node/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node22/tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "moduleDetection": "force",
6 | "erasableSyntaxOnly": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/logfire-api/vite.config.ts:
--------------------------------------------------------------------------------
1 | import defineConfig from '@pydantic/logfire-tooling-config/vite-config'
2 | import { resolve } from 'node:path'
3 |
4 | export default defineConfig(resolve(__dirname, 'src/index.ts'))
5 |
--------------------------------------------------------------------------------
/packages/logfire-browser/vite.config.ts:
--------------------------------------------------------------------------------
1 | import defineConfig from '@pydantic/logfire-tooling-config/vite-config'
2 | import { resolve } from 'node:path'
3 |
4 | export default defineConfig(resolve(__dirname, 'src/index.ts'), ['logfire'])
5 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/api/hello/route.ts:
--------------------------------------------------------------------------------
1 | import * as logfire from "logfire";
2 |
3 | export async function GET() {
4 | logfire.info("server endpoint");
5 | return Response.json({ message: "Hello World!" });
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/packages/logfire-node/vite.config.ts:
--------------------------------------------------------------------------------
1 | import defineConfig from '@pydantic/logfire-tooling-config/vite-config'
2 | import { resolve } from 'node:path'
3 |
4 | export default defineConfig(resolve(__dirname, 'src/index.ts'), ['logfire', 'picocolors'])
5 |
--------------------------------------------------------------------------------
/examples/express/.env.example:
--------------------------------------------------------------------------------
1 | # Used for reporting traces to Logfire
2 | # Change the URL if you're using a different Logfire instance
3 | # LOGFIRE_BASE_URL=https://logfire-api.pydantic.dev/
4 | LOGFIRE_WRITE_TOKEN=your-write-token
5 | EXPRESS_PORT=8080
6 |
--------------------------------------------------------------------------------
/examples/nextjs/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | devIndicators: false,
6 | reactStrictMode: true,
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/examples/node/.env.example:
--------------------------------------------------------------------------------
1 | # Used for reporting traces to Logfire
2 | # Change the URL if you're using a different Logfire instance
3 | # LOGFIRE_BASE_URL=https://logfire-api.pydantic.dev/
4 | LOGFIRE_WRITE_TOKEN=your-write-token
5 | EXPRESS_PORT=8080
6 |
--------------------------------------------------------------------------------
/packages/logfire-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "@pydantic/logfire-tooling-config/tsconfig.base.json",
4 | "include": ["src", "vite.config.ts", "eslint.config.mjs", "prettier.config.mjs"]
5 | }
6 |
--------------------------------------------------------------------------------
/examples/nextjs/app/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function RootLayout({
2 | children,
3 | }: Readonly<{
4 | children: React.ReactNode;
5 | }>) {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/packages/logfire-node/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "@pydantic/logfire-tooling-config/tsconfig.base.json",
4 | "include": ["src", "vite.config.ts", "eslint.config.mjs", "prettier.config.mjs"]
5 | }
6 |
--------------------------------------------------------------------------------
/examples/deno-project/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | OTEL_DENO=true \
3 | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://logfire-api.pydantic.dev/v1/traces \
4 | OTEL_EXPORTER_OTLP_HEADERS='Authorization=your-token' \
5 | deno run --unstable-otel --allow-net main.ts
6 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "@pydantic/logfire-tooling-config/tsconfig.base.json",
4 | "include": ["src", "vite.config.ts", "eslint.config.mjs", "prettier.config.mjs"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/vite.config.ts:
--------------------------------------------------------------------------------
1 | import defineConfig from '@pydantic/logfire-tooling-config/vite-config'
2 | import { resolve } from 'node:path'
3 |
4 | export default defineConfig(resolve(__dirname, 'src/index.ts'), ['@pydantic/otel-cf-workers', 'logfire'])
5 |
--------------------------------------------------------------------------------
/examples/cf-worker/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = tab
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.yml]
12 | indent_style = space
13 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = tab
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.yml]
12 | indent_style = space
13 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = tab
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.yml]
12 | indent_style = space
13 |
--------------------------------------------------------------------------------
/examples/cf-worker/vitest.config.mts:
--------------------------------------------------------------------------------
1 | import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
2 |
3 | export default defineWorkersConfig({
4 | test: {
5 | poolOptions: {
6 | workers: {
7 | wrangler: { configPath: './wrangler.jsonc' },
8 | },
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/vitest.config.mts:
--------------------------------------------------------------------------------
1 | import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
2 |
3 | export default defineWorkersConfig({
4 | test: {
5 | poolOptions: {
6 | workers: {
7 | wrangler: { configPath: './wrangler.jsonc' },
8 | },
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/src/index.ts:
--------------------------------------------------------------------------------
1 | import { exportTailEventsToLogfire } from '@pydantic/logfire-cf-workers';
2 |
3 | export interface Env {
4 | [key: string]: string;
5 | }
6 |
7 | export default {
8 | async tail(events, env) {
9 | await exportTailEventsToLogfire(events, env);
10 | },
11 | } satisfies ExportedHandler;
12 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "restricted",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/VoidTraceExporter.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import { SpanExporter } from '@opentelemetry/sdk-trace-base'
3 |
4 | export class VoidTraceExporter implements SpanExporter {
5 | export(): void {}
6 | async forceFlush?(): Promise {}
7 | async shutdown(): Promise {}
8 | }
9 |
--------------------------------------------------------------------------------
/packages/logfire-api/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/eslint-config'
2 |
3 | export default [
4 | ...baseConfig,
5 | {
6 | languageOptions: {
7 | parserOptions: {
8 | projectService: true,
9 | tsconfigRootDir: import.meta.dirname,
10 | },
11 | },
12 | },
13 | ]
14 |
--------------------------------------------------------------------------------
/packages/logfire-node/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/eslint-config'
2 |
3 | export default [
4 | ...baseConfig,
5 | {
6 | languageOptions: {
7 | parserOptions: {
8 | projectService: true,
9 | tsconfigRootDir: import.meta.dirname,
10 | },
11 | },
12 | },
13 | ]
14 |
--------------------------------------------------------------------------------
/packages/logfire-browser/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/eslint-config'
2 |
3 | export default [
4 | ...baseConfig,
5 | {
6 | languageOptions: {
7 | parserOptions: {
8 | projectService: true,
9 | tsconfigRootDir: import.meta.dirname,
10 | },
11 | },
12 | },
13 | ]
14 |
--------------------------------------------------------------------------------
/packages/logfire-browser/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "@pydantic/logfire-tooling-config/tsconfig.base.json",
4 | "include": ["src", "vite.config.ts", "eslint.config.mjs", "prettier.config.mjs"],
5 | "compilerOptions": {
6 | "types": ["../../node_modules/user-agent-data-types"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/eslint-config'
2 |
3 | export default [
4 | ...baseConfig,
5 | {
6 | languageOptions: {
7 | parserOptions: {
8 | projectService: true,
9 | tsconfigRootDir: import.meta.dirname,
10 | },
11 | },
12 | },
13 | ]
14 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/VoidMetricExporter.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import { PushMetricExporter } from '@opentelemetry/sdk-metrics'
3 |
4 | export class VoidMetricExporter implements PushMetricExporter {
5 | export(): void {}
6 | async forceFlush(): Promise {}
7 | async shutdown(): Promise {}
8 | }
9 |
--------------------------------------------------------------------------------
/examples/nextjs/app/page.tsx:
--------------------------------------------------------------------------------
1 | import * as logfire from 'logfire'
2 |
3 | /** Add your relevant code here for the issue to reproduce */
4 | export default async function Home() {
5 | return logfire.span('Info parent span', {}, { level: logfire.Level.Info }, async () => {
6 | logfire.info('child span');
7 | return Hello
;
8 | })
9 | }
10 |
--------------------------------------------------------------------------------
/examples/browser/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/logfire-api/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/logfire-node/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | *.tgz
26 |
--------------------------------------------------------------------------------
/packages/logfire-browser/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | *.tgz
26 |
--------------------------------------------------------------------------------
/examples/nextjs/.codesandbox/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "setupTasks": [
3 | {
4 | "name": "Install Dependencies",
5 | "command": "pnpm install"
6 | }
7 | ],
8 | "tasks": {
9 | "dev": {
10 | "name": "dev",
11 | "command": "pnpm update next@canary && pnpm dev",
12 | "runAtStart": true,
13 | "restartOn": {
14 | "clone": true
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/nextjs/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import type { NextRequest } from 'next/server'
3 |
4 | // This function can be marked `async` if using `await` inside
5 | export function middleware(request: NextRequest) {
6 | return NextResponse.redirect(new URL('/', request.url))
7 | }
8 |
9 | // See "Matching Paths" below to learn more
10 | export const config = {
11 | matcher: '/about/:path*',
12 | }
13 |
--------------------------------------------------------------------------------
/packages/logfire-api/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — Uncomplicated Observability — JavaScript SDK
2 |
3 | From the team behind [Pydantic Validation](https://pydantic.dev/), **Pydantic Logfire** is an observability platform built on the same belief as our open source library — that the most powerful tools can be easy to use.
4 |
5 | Check the [Github Repository README](https://github.com/pydantic/logfire-js) for more information on how to use the SDK.
6 |
--------------------------------------------------------------------------------
/.claude/settings.local.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissions": {
3 | "allow": [
4 | "Bash(cat:*)",
5 | "WebSearch",
6 | "Bash(npm run build)",
7 | "Bash(npm run build:*)",
8 | "Bash(npx changeset:*)",
9 | "Skill(prp-workflow)",
10 | "Bash(tree:*)",
11 | "Bash(git mv:*)",
12 | "Bash(npm install)",
13 | "Bash(npm run test:*)",
14 | "Bash(npm run ci:*)"
15 | ],
16 | "deny": [],
17 | "ask": []
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/worker-configuration.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by Wrangler by running `wrangler types --include-runtime=false` (hash: 2905fd8e181cd2f4083a615fa51f1913)
2 | // After adding bindings to `wrangler.jsonc`, regenerate this interface via `npm run cf-typegen`
3 | declare namespace Cloudflare {
4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/no-empty-object-type
5 | interface Env {
6 | }
7 | }
8 | interface Env extends Cloudflare.Env {}
9 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the Pydantic Logfire JavaScript SDK - instructions
2 |
3 | 1. Fork and clone the repository.
4 | 2. Run `npm install`.
5 | 3. Start the relevant example from `examples` and modify it accordingly to illustrate the change/feature you're working on.
6 | 4. Modify the package(s) source code located in `packages`, run `npm run build` to rebuild.
7 | 5. Commit your changes and push to your fork.
8 | 6. Submit a pull request.
9 |
10 | You're now set up to start contributing!
11 |
--------------------------------------------------------------------------------
/examples/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/examples/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-nextjs-example",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev --turbopack",
6 | "build": "next build",
7 | "start": "next start"
8 | },
9 | "dependencies": {
10 | "@vercel/otel": "^1.10.3",
11 | "logfire": "*",
12 | "next": "15.2.1",
13 | "react": "^19.0.0",
14 | "react-dom": "^19.0.0"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^22",
18 | "@types/react": "^19",
19 | "typescript": "^5"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/wrangler.jsonc:
--------------------------------------------------------------------------------
1 | /**
2 | * For more details on how to configure Wrangler, refer to:
3 | * https://developers.cloudflare.com/workers/wrangler/configuration/
4 | */
5 | {
6 | "$schema": "https://unpkg.com/wrangler@latest/config-schema.json",
7 | "name": "cloudflare-worker",
8 | "main": "src/index.ts",
9 | "compatibility_date": "2025-03-11",
10 | "compatibility_flags": ["nodejs_compat"],
11 | "observability": {
12 | "enabled": true,
13 | },
14 | "tail_consumers": [
15 | {
16 | "service": "example-tail-worker",
17 | },
18 | ],
19 | }
20 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/cf-tail-worker",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "deploy": "wrangler deploy",
7 | "dev": "wrangler dev",
8 | "start": "wrangler dev",
9 | "cf-typegen": "wrangler types"
10 | },
11 | "devDependencies": {
12 | "@cloudflare/vitest-pool-workers": "^0.8.19",
13 | "@cloudflare/workers-types": "^4.20250425.0",
14 | "typescript": "^5.5.2",
15 | "vitest": "~3.0.7",
16 | "wrangler": "^4.13.2"
17 | },
18 | "dependencies": {
19 | "@pydantic/logfire-cf-workers": "*"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/cf-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/cf-worker",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "deploy": "wrangler deploy",
7 | "dev": "wrangler dev",
8 | "start": "wrangler dev",
9 | "cf-typegen": "wrangler types"
10 | },
11 | "devDependencies": {
12 | "@cloudflare/vitest-pool-workers": "^0.7.5",
13 | "@cloudflare/workers-types": "^4.20250311.0",
14 | "typescript": "^5.5.2",
15 | "vitest": "~3.0.7",
16 | "wrangler": "^4.0.0"
17 | },
18 | "dependencies": {
19 | "logfire": "*",
20 | "@pydantic/logfire-cf-workers": "*"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | webpack: (config, { isServer }) => {
6 | if (!isServer) {
7 | config.resolve = {
8 | ...config.resolve,
9 | fallback: {
10 | ...config.resolve.fallback,
11 | fs: false,
12 | },
13 | };
14 | }
15 | config.module = {
16 | ...config.module,
17 | exprContextCritical: false,
18 | };
19 | return config;
20 | },
21 | };
22 |
23 | export default nextConfig;
24 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import dynamic from "next/dynamic";
3 | import HelloButton from "./components/HelloButton";
4 |
5 | const ClientInstrumentationProvider = dynamic(
6 | () => import("./components/ClientInstrumentationProvider"),
7 | { ssr: false },
8 | );
9 | export default function Home() {
10 | return (
11 |
12 | Next.js API Example
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-cloudflare-worker-example",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "deploy": "wrangler deploy",
7 | "dev": "wrangler dev",
8 | "start": "wrangler dev",
9 | "cf-typegen": "wrangler types"
10 | },
11 | "devDependencies": {
12 | "@cloudflare/vitest-pool-workers": "^0.7.5",
13 | "@cloudflare/workers-types": "^4.20250311.0",
14 | "typescript": "^5.5.2",
15 | "vitest": "~3.0.7",
16 | "wrangler": "^4.0.0"
17 | },
18 | "dependencies": {
19 | "@microlabs/otel-cf-workers": "^1.0.0-rc.49",
20 | "logfire": "*",
21 | "@pydantic/logfire-cf-workers": "*"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/agent/prompts/add-default-export.md:
--------------------------------------------------------------------------------
1 | # Default export addition
2 |
3 | I need to add a default export to all packages in this monorepo. The default export object should be the same as the star import. For example, the following two codes are equivalent:
4 |
5 | ```ts
6 | import * as logfire from 'logfire';
7 | ```
8 |
9 | ```ts
10 | import logfire from 'logfire';
11 | ```
12 |
13 | Implement this for every package in the monorepo. Do not touch the examples.
14 |
15 | ## Details
16 |
17 | Explicitly construct the default export object using the current imports. This should happen in the index.ts files.
18 |
19 | ## Testing
20 |
21 | Test this feature by rebuilding the packages and verifying the resulting bundles.
22 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | export function middleware(request: NextRequest) {
4 | const url = request.nextUrl.clone();
5 |
6 | if (url.pathname === "/client-traces") {
7 | const requestHeaders = new Headers(request.headers);
8 |
9 | requestHeaders.set("Authorization", process.env.LOGFIRE_TOKEN!);
10 |
11 | return NextResponse.rewrite(
12 | new URL(process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT!),
13 | {
14 | request: {
15 | headers: requestHeaders,
16 | }
17 | },
18 | );
19 | }
20 | }
21 |
22 | export const config = {
23 | matcher: "/client-traces/:path*",
24 | };
25 |
--------------------------------------------------------------------------------
/examples/browser/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "verbatimModuleSyntax": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "erasableSyntaxOnly": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedSideEffectImports": true
23 | },
24 | "include": ["src"]
25 | }
26 |
--------------------------------------------------------------------------------
/examples/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates a new object by excluding specified keys from the original object
3 | * @param obj The source object
4 | * @param keys Array of keys to exclude from the result
5 | * @returns A new object without the specified keys
6 | */
7 | export function omit(obj: T, keys: K[]): Omit {
8 | const keysToExclude = new Set(keys)
9 | return Object.fromEntries(Object.entries(obj).filter(([key]) => !keysToExclude.has(key as K))) as Omit
10 | }
11 |
12 | export function removeEmptyKeys>(dict: T): T {
13 | return Object.fromEntries(Object.entries(dict).filter(([, value]) => value !== undefined && value !== null)) as T
14 | }
15 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/logfireApiConfig.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest'
2 |
3 | import { resolveBaseUrl } from './logfireApiConfig'
4 |
5 | test('returns the passed url', () => {
6 | const baseUrl = resolveBaseUrl({}, 'https://example.com', 'token')
7 | expect(baseUrl).toBe('https://example.com')
8 | })
9 |
10 | test('resolves the US base url from the token', () => {
11 | const baseUrl = resolveBaseUrl({}, undefined, 'pylf_v1_us_1234567890')
12 | expect(baseUrl).toBe('https://logfire-us.pydantic.dev')
13 | })
14 |
15 | test('resolves the EU base url from the token', () => {
16 | const baseUrl = resolveBaseUrl({}, undefined, 'pylf_v1_eu_mFMvBQ7BWLPJ0fHYBGLVBmJ70TpkhlskgRLng0jFsb3n')
17 | expect(baseUrl).toBe('https://logfire-eu.pydantic.dev')
18 | })
19 |
--------------------------------------------------------------------------------
/examples/browser/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browser",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "proxy": "tsx --env-file=.env src/proxy.ts",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@opentelemetry/auto-instrumentations-web": "^0.49.0",
14 | "cors": "^2.8.5",
15 | "express": "^4.21.2",
16 | "express-http-proxy": "^2.1.1",
17 | "http-proxy-middleware": "^2.0.6"
18 | },
19 | "devDependencies": {
20 | "logfire": "*",
21 | "@pydantic/logfire-browser": "*",
22 | "@types/cors": "^2.8.17",
23 | "@types/express": "^4.17.21",
24 | "tsx": "^4.7.0",
25 | "typescript": "~5.8.3",
26 | "vite": "^7.0.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background: #ffffff;
3 | --foreground: #171717;
4 | }
5 |
6 | @media (prefers-color-scheme: dark) {
7 | :root {
8 | --background: #0a0a0a;
9 | --foreground: #ededed;
10 | }
11 | }
12 |
13 | html,
14 | body {
15 | max-width: 100vw;
16 | overflow-x: hidden;
17 | }
18 |
19 | body {
20 | color: var(--foreground);
21 | background: var(--background);
22 | font-family: Arial, Helvetica, sans-serif;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 |
27 | * {
28 | box-sizing: border-box;
29 | padding: 0;
30 | margin: 0;
31 | }
32 |
33 | a {
34 | color: inherit;
35 | text-decoration: none;
36 | }
37 |
38 | @media (prefers-color-scheme: dark) {
39 | html {
40 | color-scheme: dark;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const geistSans = Geist({
6 | variable: "--font-geist-sans",
7 | subsets: ["latin"],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: "--font-geist-mono",
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: "Create Next App",
17 | description: "Generated by create next app",
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: Readonly<{
23 | children: React.ReactNode;
24 | }>) {
25 | return (
26 |
27 |
28 | {children}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "tasks": {
4 | "build": {
5 | "dependsOn": [
6 | "^build"
7 | ],
8 | "outputs": [
9 | "dist/**",
10 | ".next/**",
11 | "build/**",
12 | "public/build/**"
13 | ]
14 | },
15 | "dev": {
16 | "persistent": true,
17 | "cache": false,
18 | "interruptible": true
19 | },
20 | "start": {
21 | "dependsOn": [
22 | "^build"
23 | ]
24 | },
25 | "test": {
26 | "dependsOn": [
27 | "^build"
28 | ]
29 | },
30 | "lint": {
31 | "dependsOn": [
32 | "^build",
33 | "^lint"
34 | ]
35 | },
36 | "typecheck": {
37 | "dependsOn": [
38 | "^build",
39 | "^typecheck"
40 | ]
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/tooling-config/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "lib": [
8 | "ES2020",
9 | "DOM",
10 | "DOM.Iterable"
11 | ],
12 | "skipLibCheck": true,
13 | /* Bundler mode */
14 | "moduleResolution": "bundler",
15 | "allowImportingTsExtensions": true,
16 | "isolatedModules": true,
17 | "moduleDetection": "force",
18 | "noEmit": true,
19 | /* Linting */
20 | "strict": true,
21 | "allowJs": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noUncheckedIndexedAccess": true,
25 | "noFallthroughCasesInSwitch": true,
26 | "noUncheckedSideEffectImports": true,
27 | "erasableSyntaxOnly": true
28 | },
29 | }
30 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/components/HelloButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState } from "react";
3 |
4 | export default function HelloButton() {
5 | const [message, setMessage] = useState("");
6 | const [loading, setLoading] = useState(false);
7 |
8 | const fetchHello = async () => {
9 | setLoading(true);
10 | try {
11 | const response = await fetch("/api/hello");
12 | const data = await response.json();
13 | setMessage(data.message);
14 | } catch {
15 | setMessage("Error fetching data");
16 | } finally {
17 | setLoading(false);
18 | }
19 | };
20 |
21 | return (
22 |
23 |
29 | {message &&
{message}
}
30 |
31 | );
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/examples/node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-node-example",
3 | "private": true,
4 | "version": "1.0.0",
5 | "main": "index.ts",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node --experimental-strip-types --disable-warning=ExperimentalWarning index.ts",
9 | "start-and-throw-error": "TRIGGER_ERROR=true node --experimental-strip-types --disable-warning=ExperimentalWarning index.ts",
10 | "debug": "node --experimental-strip-types --disable-warning=ExperimentalWarning --inspect-brk index.ts",
11 | "typecheck": "tsc"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "description": "",
17 | "dependencies": {
18 | "dotenv": "^16.4.7",
19 | "@pydantic/logfire-node": "*"
20 | },
21 | "devDependencies": {
22 | "@opentelemetry/semantic-conventions": "^1.30.0",
23 | "@tsconfig/node22": "^22.0.0",
24 | "typescript": "^5.8.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/tooling-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-tooling-config",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "private": true,
6 | "exports": {
7 | "./eslint-config": "./eslint-config.mjs",
8 | "./prettier-config": "./prettier-config.mjs",
9 | "./vite-config": "./vite-config.mjs",
10 | "./tsconfig.base.json": "./tsconfig.base.json"
11 | },
12 | "devDependencies": {
13 | "@eslint/js": "^9.22.0",
14 | "@typescript-eslint/utils": "^8.26.1",
15 | "eslint": "^9.22.0",
16 | "eslint-config-prettier": "^10.1.1",
17 | "eslint-plugin-perfectionist": "^4.10.1",
18 | "eslint-plugin-prettier": "^5.2.3",
19 | "eslint-plugin-turbo": "^2.4.4",
20 | "globals": "^16.0.0",
21 | "neostandard": "^0.12.1",
22 | "prettier": "3.5.3",
23 | "typescript": "^5.8.2",
24 | "typescript-eslint": "^8.26.1",
25 | "vite": "^6.2.1",
26 | "vite-plugin-dts": "^4.5.3",
27 | "vitest": "^3.0.8"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/constants.ts:
--------------------------------------------------------------------------------
1 | // Constants used by the formatter and scrubber
2 |
3 | /** Maximum length for formatted values in messages */
4 | export const MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT = 2000
5 | const LOGFIRE_ATTRIBUTES_NAMESPACE = 'logfire'
6 | export const ATTRIBUTES_LEVEL_KEY = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.level_num`
7 | export const ATTRIBUTES_SPAN_TYPE_KEY = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.span_type`
8 | export const ATTRIBUTES_TAGS_KEY = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.tags`
9 | export const ATTRIBUTES_MESSAGE_TEMPLATE_KEY = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.msg_template`
10 | export const ATTRIBUTES_MESSAGE_KEY = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.msg`
11 |
12 | /** Key for storing scrubbed attributes information */
13 | export const ATTRIBUTES_SCRUBBED_KEY = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.scrubbed`
14 | export const DEFAULT_OTEL_SCOPE = 'logfire'
15 | export const JSON_SCHEMA_KEY = 'logfire.json_schema'
16 | export const JSON_NULL_FIELDS_KEY = 'logfire.null_args'
17 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/ULIDGenerator.ts:
--------------------------------------------------------------------------------
1 | import { RandomIdGenerator } from '@opentelemetry/sdk-trace-base'
2 |
3 | export class ULIDGenerator extends RandomIdGenerator {
4 | override generateTraceId = () => {
5 | const id = ulid().toString(16).padStart(32, '0')
6 | return id
7 | }
8 | }
9 |
10 | // JS port of https://github.com/pydantic/logfire/blob/main/logfire/_internal/ulid.py without the parameters
11 | function ulid(): bigint {
12 | // Timestamp: first 6 bytes of the ULID (48 bits)
13 | // Note that it's not important that this timestamp is super precise or unique.
14 | // It just needs to be roughly monotonically increasing so that the ULID is sortable, at least for our purposes.
15 | let result = BigInt(Date.now())
16 |
17 | // Randomness: next 10 bytes of the ULID (80 bits)
18 | const randomness = crypto.getRandomValues(new Uint8Array(10))
19 | for (const segment of randomness) {
20 | result <<= BigInt(8)
21 | result |= BigInt(segment)
22 | }
23 |
24 | return result
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-monorepo",
3 | "version": "1.0.0",
4 | "private": true,
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "turbo watch dev",
8 | "build": "turbo build",
9 | "test": "turbo test",
10 | "release": "turbo build && npx @changesets/cli publish",
11 | "changeset-add": "npx @changesets/cli add",
12 | "ci": "turbo typecheck lint test"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "description": "",
18 | "workspaces": [
19 | "examples/*",
20 | "examples/cloudflare-tail-worker/*",
21 | "packages/*"
22 | ],
23 | "devDependencies": {
24 | "@changesets/cli": "^2.27.12",
25 | "turbo": "^2.4.4"
26 | },
27 | "engines": {
28 | "node": "22"
29 | },
30 | "devEngines": {
31 | "runtime": {
32 | "name": "node",
33 | "onFail": "error"
34 | },
35 | "packageManager": {
36 | "name": "npm",
37 | "onFail": "error"
38 | }
39 | },
40 | "packageManager": "npm@10.9.2"
41 | }
42 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/cf-worker/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to Cloudflare Workers! This is your first worker.
3 | *
4 | * - Run `npm run dev` in your terminal to start a development server
5 | * - Open a browser tab at http://localhost:8787/ to see your worker in action
6 | * - Run `npm run deploy` to publish your worker
7 | *
8 | * Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the
9 | * `Env` object can be regenerated with `npm run cf-typegen`.
10 | *
11 | * Learn more at https://developers.cloudflare.com/workers/
12 | */
13 | import * as logfire from 'logfire';
14 | import { instrument } from '@pydantic/logfire-cf-workers';
15 |
16 | const handler = {
17 | async fetch(): Promise {
18 | logfire.info('span from inside the worker body', { foo: 'bar' });
19 | return new Response('Hello World!');
20 | },
21 | } satisfies ExportedHandler;
22 |
23 | export default instrument(handler, {
24 | service: {
25 | name: 'cloudflare-worker',
26 | namespace: '',
27 | version: '1.0.0',
28 | },
29 | console: true,
30 | });
31 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/metricExporter.ts:
--------------------------------------------------------------------------------
1 | import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'
2 | import { PeriodicExportingMetricReader, PeriodicExportingMetricReaderOptions, PushMetricExporter } from '@opentelemetry/sdk-metrics'
3 |
4 | import { logfireConfig } from './logfireConfig'
5 | import { VoidMetricExporter } from './VoidMetricExporter'
6 |
7 | export type PeriodicMetricReaderOptions = Omit
8 |
9 | export function metricExporter(): PushMetricExporter {
10 | if (!logfireConfig.sendToLogfire) {
11 | return new VoidMetricExporter()
12 | }
13 |
14 | const token = logfireConfig.token
15 | if (!token) {
16 | throw new Error('Logfire token is required')
17 | }
18 | return new OTLPMetricExporter({
19 | headers: logfireConfig.authorizationHeaders,
20 | url: logfireConfig.metricExporterUrl,
21 | })
22 | }
23 |
24 | export function periodicMetricReader(options?: PeriodicMetricReaderOptions) {
25 | return new PeriodicExportingMetricReader({
26 | exporter: metricExporter(),
27 | ...options,
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/examples/browser/src/proxy.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 |
4 | const app = express();
5 | const PORT = 8989;
6 |
7 | // Enable CORS - handle origins dynamically to avoid wildcard issues with credentials
8 | app.use(cors({
9 | origin: '*',
10 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
11 | credentials: true
12 | }));
13 |
14 | // Parse JSON bodies
15 | app.use(express.json());
16 |
17 | const logfireUrl = process.env.LOGFIRE_URL || 'http://localhost:4318/v1/traces';
18 | const token = process.env.LOGFIRE_TOKEN || ''
19 |
20 |
21 | // Single endpoint: POST /client-traces
22 | app.post('/client-traces', async (req, res) => {
23 | const response = await fetch(logfireUrl, {
24 | method: 'POST',
25 | headers: {
26 | 'Content-Type': 'application/json',
27 | 'Authorization': token
28 | },
29 | body: JSON.stringify(req.body),
30 | })
31 | res.status(response.status).send(response.body)
32 | });
33 |
34 | // Start the server
35 | app.listen(PORT, () => {
36 | console.log(`Server running on port ${PORT}, proxying to ${logfireUrl}`);
37 | });
38 |
39 | export default app;
40 |
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 - present Pydantic Services inc.
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.
22 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/components/ClientInstrumentationProvider.tsx:
--------------------------------------------------------------------------------
1 | import { getWebAutoInstrumentations } from "@opentelemetry/auto-instrumentations-web";
2 | import * as logfire from '@pydantic/logfire-browser';
3 | import { ReactNode, useEffect, useRef } from "react";
4 |
5 |
6 | interface ClientInstrumentationProviderProps {
7 | children: ReactNode;
8 | }
9 |
10 | export default function ClientInstrumentationProvider({ children }: ClientInstrumentationProviderProps) {
11 | const logfireConfigured = useRef(false);
12 |
13 | useEffect(() => {
14 | const url = new URL(window.location.href);
15 | url.pathname = "/client-traces";
16 | if (!logfireConfigured.current) {
17 | logfire.configure({
18 | traceUrl: url.toString(),
19 | serviceName: process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME,
20 | serviceVersion: process.env.NEXT_PUBLIC_OTEL_SERVICE_VERSION,
21 | instrumentations: [
22 | getWebAutoInstrumentations()
23 | ],
24 | diagLogLevel: logfire.DiagLogLevel.ALL
25 | })
26 | logfireConfigured.current = true;
27 | }
28 | }, []);
29 |
30 | return children;
31 | }
32 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/README.md:
--------------------------------------------------------------------------------
1 | # Example for a Next.js client/server distributed OTel instrumentation with Pydantic Logfire
2 |
3 | The example showcases how a fetch request initiated from the browser can propagate to the server and then to a third-party service, all while being instrumented with OpenTelemetry. The example uses the Pydantic Logfire OTel SDK for both the client and server sides.
4 |
5 | ## Highlights
6 |
7 | - The `ClientInstrumentationProvider` is a client-only component that instruments the browser fetch.
8 | - To avoid exposing the write token, the middleware.ts proxies the logfire `/v1/traces` request.
9 | - The instrumentation.ts file is the standard `@vercel/otel` setup.
10 | - The `.env` should look like this:
11 |
12 | ```sh
13 | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://logfire-api.pydantic.dev/v1/traces
14 | OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://logfire-api.pydantic.dev/v1/metrics
15 | OTEL_EXPORTER_OTLP_HEADERS='Authorization=your-token'
16 | LOGFIRE_TOKEN='your-token'
17 | ```
18 |
19 | NOTE: alternatively, if you're not sure about the connection between the client and the server, you can host the proxy at a different location (e.g. Cloudflare).
20 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to Cloudflare Workers! This is your first worker.
3 | *
4 | * - Run `npm run dev` in your terminal to start a development server
5 | * - Open a browser tab at http://localhost:8787/ to see your worker in action
6 | * - Run `npm run deploy` to publish your worker
7 | *
8 | * Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the
9 | * `Env` object can be regenerated with `npm run cf-typegen`.
10 | *
11 | * Learn more at https://developers.cloudflare.com/workers/
12 | */
13 | import * as logfire from 'logfire';
14 | import { instrumentTail } from '@pydantic/logfire-cf-workers';
15 |
16 | const handler = {
17 | async fetch(): Promise {
18 | logfire.info('span1');
19 | await fetch('https://example.com/1');
20 | logfire.info('span2');
21 | await fetch('https://example.com/2');
22 | // await new Promise((resolve) => setTimeout(resolve, 100));
23 | return new Response('Hello World!');
24 | },
25 | } satisfies ExportedHandler;
26 |
27 | export default instrumentTail(handler, {
28 | service: {
29 | name: 'cloudflare-worker',
30 | namespace: '',
31 | version: '1.0.0',
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/examples/express/app.ts:
--------------------------------------------------------------------------------
1 | import type { Express, Request, Response } from "express";
2 | import express from "express";
3 | import * as logfire from "@pydantic/logfire-node";
4 |
5 | const PORT: number = parseInt(process.env.EXPRESS_PORT || "8080");
6 | const app: Express = express();
7 |
8 | function getRandomNumber(min: number, max: number) {
9 | return Math.floor(Math.random() * (max - min) + min);
10 | }
11 |
12 | app.get("/rolldice", (req, res) => {
13 | // read the query parameter error
14 | const error = req.query.error;
15 | if (error) {
16 | throw new Error("An error occurred");
17 | }
18 |
19 | logfire.span(
20 | "parent-span",
21 | {},
22 | {},
23 | async (parentSpan) => {
24 | logfire.info("child span");
25 | parentSpan.end();
26 | },
27 | );
28 |
29 | res.send(getRandomNumber(1, 6).toString());
30 | });
31 |
32 | // Report an error to Logfire, using the Express error handler.
33 | app.use((err: Error, _req: Request, res: Response, _next: () => unknown) => {
34 | logfire.reportError(err.message, err);
35 | res.status(500);
36 | res.send("An error occured");
37 | });
38 |
39 | app.listen(PORT, () => {
40 | console.log(`Listening for requests on http://localhost:${PORT}/rolldice`);
41 | });
42 |
--------------------------------------------------------------------------------
/packages/tooling-config/eslint-config.mjs:
--------------------------------------------------------------------------------
1 | import pluginJs from '@eslint/js'
2 | import eslintConfigPrettier from 'eslint-config-prettier/flat'
3 | import perfectionist from 'eslint-plugin-perfectionist'
4 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
5 | import globals from 'globals'
6 | import neostandard, { resolveIgnoresFromGitignore } from 'neostandard'
7 | import tseslint from 'typescript-eslint'
8 | import turboPlugin from "eslint-plugin-turbo";
9 |
10 | export default tseslint.config(
11 | pluginJs.configs.recommended,
12 | tseslint.configs.strictTypeChecked,
13 | tseslint.configs.stylisticTypeChecked,
14 | perfectionist.configs['recommended-natural'],
15 | neostandard({ noJsx: true, noStyle: true }),
16 | eslintPluginPrettierRecommended,
17 | eslintConfigPrettier,
18 | { files: ['src/*.{js,mjs,cjs,ts}', 'eslint.config.mjs', 'vite.config.ts'] },
19 | {
20 | languageOptions: {
21 | globals: { ...globals.browser, ...globals.node },
22 | },
23 | },
24 | {
25 | plugins: {
26 | turbo: turboPlugin,
27 | },
28 | rules: {
29 | "turbo/no-undeclared-env-vars": "off",
30 | "perfectionist/sort-modules": "off",
31 | },
32 | },
33 | { ignores: resolveIgnoresFromGitignore() }
34 | )
35 |
--------------------------------------------------------------------------------
/examples/express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-express-example",
3 | "private": true,
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node --experimental-strip-types --disable-warning=ExperimentalWarning --import ./instrumentation.ts app.ts"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "description": "",
14 | "dependencies": {
15 | "@opentelemetry/api": "^1.9.0",
16 | "@opentelemetry/auto-instrumentations-node": "^0.49.2",
17 | "@opentelemetry/context-async-hooks": "^1.26.0",
18 | "@opentelemetry/exporter-metrics-otlp-proto": "^0.53.0",
19 | "@opentelemetry/exporter-trace-otlp-proto": "^0.53.0",
20 | "@opentelemetry/resources": "^1.26.0",
21 | "@opentelemetry/sdk-metrics": "^1.26.0",
22 | "@opentelemetry/sdk-node": "^0.53.0",
23 | "@opentelemetry/sdk-trace-node": "^1.26.0",
24 | "@opentelemetry/semantic-conventions": "^1.27.0",
25 | "@types/express": "^4.17.21",
26 | "@types/node": "^22.5.1",
27 | "dotenv": "^16.4.7",
28 | "express": "^4.21.2",
29 | "@pydantic/logfire-node": "*",
30 | "tsx": "^4.19.0",
31 | "typescript": "^5.5.4"
32 | },
33 | "devDependencies": {
34 | "@tsconfig/node20": "^20.1.4"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/index.ts:
--------------------------------------------------------------------------------
1 | import { DiagLogLevel } from '@opentelemetry/api'
2 | import {
3 | configureLogfireApi,
4 | debug,
5 | error,
6 | fatal,
7 | info,
8 | Level,
9 | log,
10 | logfireApiConfig,
11 | LogfireAttributeScrubber,
12 | NoopAttributeScrubber,
13 | notice,
14 | reportError,
15 | resolveBaseUrl,
16 | resolveSendToLogfire,
17 | serializeAttributes,
18 | span,
19 | startSpan,
20 | trace,
21 | ULIDGenerator,
22 | warning,
23 | } from 'logfire'
24 |
25 | // Import all exports to construct default export
26 | import * as logfireConfigExports from './logfireConfig'
27 |
28 | export * from './logfireConfig'
29 | export { DiagLogLevel } from '@opentelemetry/api'
30 | export * from 'logfire'
31 |
32 | // Create default export by listing all exports explicitly
33 | export default {
34 | ...logfireConfigExports,
35 | configureLogfireApi,
36 | debug,
37 | DiagLogLevel,
38 | error,
39 | fatal,
40 | info,
41 | // Re-export all from logfire
42 | Level,
43 | log,
44 | logfireApiConfig,
45 | LogfireAttributeScrubber,
46 | NoopAttributeScrubber,
47 | notice,
48 | reportError,
49 | resolveBaseUrl,
50 | resolveSendToLogfire,
51 | serializeAttributes,
52 | span,
53 | startSpan,
54 | trace,
55 | ULIDGenerator,
56 | warning,
57 | }
58 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/exportTailEventsToLogfire.ts:
--------------------------------------------------------------------------------
1 | import { resolveBaseUrl } from 'logfire'
2 |
3 | // simplified interface from CF
4 | export interface TraceItem {
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
6 | logs: { message: any[] }[]
7 | }
8 |
9 | export async function exportTailEventsToLogfire(events: TraceItem[], env: Record) {
10 | const token = env.LOGFIRE_TOKEN
11 | if (!token) {
12 | console.warn('No token provided, not sending payload to Logfire')
13 | return
14 | }
15 | const url = resolveBaseUrl(env, undefined, token)
16 |
17 | for (const event of events) {
18 | for (const log of event.logs) {
19 | if (Array.isArray(log.message)) {
20 | for (const entry of log.message) {
21 | if ('resourceSpans' in entry) {
22 | try {
23 | return await fetch(`${url}/v1/traces`, {
24 | body: JSON.stringify(entry),
25 | headers: {
26 | Authorization: token,
27 | 'Content-Type': 'application/json',
28 | },
29 | method: 'POST',
30 | })
31 | } catch (e) {
32 | console.error(e)
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/nextjs-client-side-instrumentation",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack --port 3001",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@opentelemetry/api": "^1.9.0",
13 | "@opentelemetry/auto-instrumentations-web": "^0.48.0",
14 | "@opentelemetry/context-zone": "^1.30.1",
15 | "@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
16 | "@opentelemetry/instrumentation": "^0.57.2",
17 | "@opentelemetry/instrumentation-fetch": "^0.57.2",
18 | "@opentelemetry/instrumentation-user-interaction": "^0.47.0",
19 | "@opentelemetry/resources": "^1.19.0",
20 | "@opentelemetry/sdk-trace-web": "^1.30.1",
21 | "@opentelemetry/semantic-conventions": "^1.32.0",
22 | "logfire": "*",
23 | "@pydantic/logfire-browser": "*",
24 | "@vercel/otel": "^1.11.0",
25 | "next": "15.3.0",
26 | "react": "^19.0.0",
27 | "react-dom": "^19.0.0"
28 | },
29 | "devDependencies": {
30 | "@eslint/eslintrc": "^3",
31 | "@types/node": "^20",
32 | "@types/react": "^19",
33 | "@types/react-dom": "^19",
34 | "eslint": "^9",
35 | "eslint-config-next": "15.3.0",
36 | "typescript": "^5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/tooling-config/vite-config.mjs:
--------------------------------------------------------------------------------
1 | import { copyFileSync } from 'node:fs'
2 | import { defineConfig } from 'vite'
3 | import dts from 'vite-plugin-dts'
4 |
5 | export default (entry, external = []) => defineConfig({
6 | build: {
7 | lib: {
8 | entry,
9 | fileName: 'index',
10 | formats: ['es', 'cjs'],
11 | },
12 | minify: true,
13 | rollupOptions: {
14 | external: (id) => {
15 | return id.startsWith('@opentelemetry') || external.includes(id)
16 | },
17 | output: {
18 | exports: 'named',
19 | },
20 | },
21 | },
22 | define: {
23 | PACKAGE_TIMESTAMP: new Date().getTime(),
24 | PACKAGE_VERSION: JSON.stringify(process.env.npm_package_version || '0.0.0'),
25 | },
26 | plugins: [
27 | dts({
28 | // https://github.com/arethetypeswrong
29 | // https://github.com/qmhc/vite-plugin-dts/issues/267#issuecomment-1786996676
30 | afterBuild: () => {
31 | // To pass publint (`npm x publint@latest`) and ensure the
32 | // package is supported by all consumers, we must export types that are
33 | // read as ESM. To do this, there must be duplicate types with the
34 | // correct extension supplied in the package.json exports field.
35 | copyFileSync('dist/index.d.ts', 'dist/index.d.cts')
36 | },
37 | compilerOptions: { skipLibCheck: true },
38 | rollupTypes: true,
39 | staticImport: true,
40 | }),
41 | ],
42 | })
43 |
44 |
--------------------------------------------------------------------------------
/examples/browser/src/main.ts:
--------------------------------------------------------------------------------
1 | import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
2 | import * as logfire from '@pydantic/logfire-browser';
3 |
4 |
5 | logfire.configure({
6 | traceUrl: 'http://localhost:8989/client-traces',
7 | serviceName: 'my-service',
8 | serviceVersion: '0.1.0',
9 | // The instrumentations to use
10 | // https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-web - for more options and configuration
11 | instrumentations: [
12 | getWebAutoInstrumentations({
13 | "@opentelemetry/instrumentation-document-load": { enabled: true },
14 | "@opentelemetry/instrumentation-user-interaction": {
15 | eventNames: ['click']
16 | },
17 | })
18 | ],
19 | // This outputs details about the generated spans in the browser console, use only in development and for troubleshooting.
20 | diagLogLevel: logfire.DiagLogLevel.ALL,
21 | batchSpanProcessorConfig: {
22 | maxExportBatchSize: 10
23 | },
24 | })
25 |
26 |
27 | document.querySelector('button')?.addEventListener('click', () => {
28 | logfire.info('Button clicked!')
29 | logfire.span('fetch wrapper',
30 | {
31 | callback: async () => { return fetch('https://jsonplaceholder.typicode.com/posts/1') }
32 | }
33 | )
34 |
35 | logfire.span('test something', {
36 | callback: async () => {
37 | const promise = await new Promise((resolve) => setTimeout(resolve, 1000))
38 | logfire.info('something!')
39 | return promise
40 | },
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request: {}
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | env:
12 | CI: true
13 | steps:
14 | - name: Begin CI...
15 | uses: actions/checkout@v4
16 |
17 | - name: Use Node 22
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: 22.x
21 |
22 | - name: Install dependencies
23 | run: |
24 | npm install
25 | npm install --workspaces
26 |
27 | - name: run CI checks
28 | run: |
29 | npm run ci
30 |
31 | release:
32 | needs: [build]
33 | runs-on: ubuntu-latest
34 | if: success() && github.ref == 'refs/heads/main'
35 | permissions:
36 | id-token: write
37 | contents: write
38 | pull-requests: write
39 | environment:
40 | name: npm
41 | steps:
42 | - name: Checkout
43 | uses: actions/checkout@v4
44 |
45 | - name: Setup Node 22
46 | uses: actions/setup-node@v4
47 | with:
48 | node-version: 22.x
49 |
50 | - name: Install dependencies
51 | run: |
52 | npm install -g npm@latest
53 | npm install
54 | npm install --workspaces
55 |
56 | - name: Release
57 | id: changesets
58 | uses: changesets/action@v1
59 | with:
60 | publish: npm run release
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
64 |
--------------------------------------------------------------------------------
/examples/node/index.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config'
2 | import * as logfire from '@pydantic/logfire-node'
3 |
4 | logfire.configure({
5 | serviceName: 'example-node-script',
6 | serviceVersion: '1.0.0',
7 | environment: 'staging',
8 | diagLogLevel: logfire.DiagLogLevel.NONE,
9 | console: false
10 | })
11 |
12 |
13 | logfire.span('Hello from Node.js, {next_player}', {
14 | 'attribute-key': 'attribute-value',
15 | next_player: '0',
16 | arr: [1, 2, 3],
17 | something: {
18 | value: [1, 2, 3],
19 | key: 'value'
20 | }
21 | }, {
22 | tags: ['example', 'example2']
23 | }, (span) => {
24 | console.log('Inside span callback');
25 | })
26 |
27 | await logfire.span('parent span', {}, {}, async (_span) => {
28 | await new Promise((resolve) => setTimeout(resolve, 1000))
29 | logfire.info('nested span')
30 | await new Promise((resolve) => setTimeout(resolve, 1000))
31 | logfire.debug('another nested span')
32 | })
33 |
34 |
35 | logfire.span('parent sync span', {}, {}, (_span) => {
36 | logfire.info('nested span')
37 | })
38 |
39 | logfire.span('parent sync span overload', {
40 | callback: (_span) => {
41 | logfire.info('nested span')
42 | }
43 | })
44 |
45 | const mySpan = logfire.startSpan('a manual parent span', { 'foo': 'foo' })
46 |
47 | logfire.info('manual child span', {}, { parentSpan: mySpan })
48 |
49 | mySpan.end()
50 |
51 | if (process.env.TRIGGER_ERROR) {
52 | try {
53 | throw new Error('This is an error for testing purposes');
54 | } catch (error) {
55 | logfire.reportError("An error occurred", error as Error);
56 | console.error("An error occurred:", error);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/packages/logfire-browser/src/OTLPTraceExporterWithDynamicHeaders.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getSharedConfigurationDefaults,
3 | mergeOtlpSharedConfigurationWithDefaults,
4 | OTLPExporterBase,
5 | OTLPExporterNodeConfigBase,
6 | } from '@opentelemetry/otlp-exporter-base'
7 | import { createOtlpXhrExportDelegate } from '@opentelemetry/otlp-exporter-base/browser-http'
8 | import { JsonTraceSerializer } from '@opentelemetry/otlp-transformer'
9 | import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-web'
10 |
11 | // https://github.com/open-telemetry/opentelemetry-js/pull/3662#issuecomment-2262808849
12 | export class OTLPTraceExporterWithDynamicHeaders extends OTLPExporterBase implements SpanExporter {
13 | constructor(config: OTLPExporterNodeConfigBase, getHeaders?: () => Record) {
14 | const sharedConfig = mergeOtlpSharedConfigurationWithDefaults(
15 | {
16 | compression: config.compression,
17 | concurrencyLimit: config.concurrencyLimit,
18 | timeoutMillis: config.timeoutMillis,
19 | },
20 | {},
21 | getSharedConfigurationDefaults()
22 | )
23 |
24 | const xhrExportConfig = {
25 | ...sharedConfig,
26 | agentOptions: { keepAlive: true },
27 | headers: () => {
28 | return {
29 | 'Content-Type': 'application/json',
30 | ...(config.headers ?? {}),
31 | ...(getHeaders ? getHeaders() : {}),
32 | }
33 | },
34 | url: config.url ?? 'http://localhost:4318/v1/traces',
35 | }
36 |
37 | super(createOtlpXhrExportDelegate(xhrExportConfig, JsonTraceSerializer))
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/cf-worker/wrangler.jsonc:
--------------------------------------------------------------------------------
1 | /**
2 | * For more details on how to configure Wrangler, refer to:
3 | * https://developers.cloudflare.com/workers/wrangler/configuration/
4 | */
5 | {
6 | "$schema": "https://unpkg.com/wrangler@latest/config-schema.json",
7 | "name": "cloudflare-worker",
8 | "main": "src/index.ts",
9 | "compatibility_date": "2025-03-11",
10 | "compatibility_flags": ["nodejs_compat"],
11 | "observability": {
12 | "enabled": true,
13 | },
14 | /**
15 | * Smart Placement
16 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
17 | */
18 | // "placement": { "mode": "smart" },
19 |
20 | /**
21 | * Bindings
22 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
23 | * databases, object storage, AI inference, real-time communication and more.
24 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/
25 | */
26 |
27 | /**
28 | * Environment Variables
29 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
30 | */
31 | "vars": {},
32 | /**
33 | * Note: Use secrets to store sensitive data.
34 | * https://developers.cloudflare.com/workers/configuration/secrets/
35 | */
36 |
37 | /**
38 | * Static Assets
39 | * https://developers.cloudflare.com/workers/static-assets/binding/
40 | */
41 | // "assets": { "directory": "./public/", "binding": "ASSETS" },
42 |
43 | /**
44 | * Service Bindings (communicate between multiple Workers)
45 | * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
46 | */
47 | // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
48 | }
49 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/wrangler.jsonc:
--------------------------------------------------------------------------------
1 | /**
2 | * For more details on how to configure Wrangler, refer to:
3 | * https://developers.cloudflare.com/workers/wrangler/configuration/
4 | */
5 | {
6 | "$schema": "node_modules/wrangler/config-schema.json",
7 | "name": "example-tail-worker",
8 | "main": "src/index.ts",
9 | "compatibility_date": "2025-04-25",
10 | "compatibility_flags": ["nodejs_compat"],
11 | "observability": {
12 | "enabled": true,
13 | },
14 | /**
15 | * Smart Placement
16 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
17 | */
18 | // "placement": { "mode": "smart" },
19 |
20 | /**
21 | * Bindings
22 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
23 | * databases, object storage, AI inference, real-time communication and more.
24 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/
25 | */
26 |
27 | /**
28 | * Environment Variables
29 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
30 | */
31 | // "vars": { "MY_VARIABLE": "production_value" },
32 | /**
33 | * Note: Use secrets to store sensitive data.
34 | * https://developers.cloudflare.com/workers/configuration/secrets/
35 | */
36 |
37 | /**
38 | * Static Assets
39 | * https://developers.cloudflare.com/workers/static-assets/binding/
40 | */
41 | // "assets": { "directory": "./public/", "binding": "ASSETS" },
42 |
43 | /**
44 | * Service Bindings (communicate between multiple Workers)
45 | * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
46 | */
47 | // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
48 | }
49 |
--------------------------------------------------------------------------------
/examples/cf-worker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
6 | "target": "es2021",
7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */
8 | "lib": ["es2021"],
9 | /* Specify what JSX code is generated. */
10 | "jsx": "react-jsx",
11 |
12 | /* Specify what module code is generated. */
13 | "module": "es2022",
14 | /* Specify how TypeScript looks up a file from a given module specifier. */
15 | "moduleResolution": "Bundler",
16 | /* Specify type package names to be included without being referenced in a source file. */
17 | "types": ["@cloudflare/workers-types"],
18 | /* Enable importing .json files */
19 | "resolveJsonModule": true,
20 |
21 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
22 | "allowJs": true,
23 | /* Enable error reporting in type-checked JavaScript files. */
24 | "checkJs": false,
25 |
26 | /* Disable emitting files from a compilation. */
27 | "noEmit": true,
28 |
29 | /* Ensure that each file can be safely transpiled without relying on other imports. */
30 | "isolatedModules": true,
31 | /* Allow 'import x from y' when a module doesn't have a default export. */
32 | "allowSyntheticDefaultImports": true,
33 | /* Ensure that casing is correct in imports. */
34 | "forceConsistentCasingInFileNames": true,
35 |
36 | /* Enable all strict type-checking options. */
37 | "strict": true,
38 |
39 | /* Skip type checking all .d.ts files. */
40 | "skipLibCheck": true
41 | },
42 | "exclude": ["test"],
43 | "include": ["worker-configuration.d.ts", "src/**/*.ts"]
44 | }
45 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — JavaScript SDK
2 |
3 | From the team behind [Pydantic Validation](https://pydantic.dev/), **Pydantic Logfire** is an observability platform built on the same belief as our open source library — that the most powerful tools can be easy to use.
4 |
5 | What sets Logfire apart:
6 |
7 | - **Simple and Powerful:** Logfire's dashboard is simple relative to the power it provides, ensuring your entire engineering team will actually use it.
8 | - **SQL:** Query your data using standard SQL — all the control and (for many) nothing new to learn. Using SQL also means you can query your data with existing BI tools and database querying libraries.
9 | - **OpenTelemetry:** Logfire is an opinionated wrapper around OpenTelemetry, allowing you to leverage existing tooling, infrastructure, and instrumentation for many common packages, and enabling support for virtually any language.
10 |
11 | See the [documentation](https://logfire.pydantic.dev/docs/) for more information.
12 |
13 | **Feel free to report issues and ask any questions about Logfire in this repository!**
14 |
15 | This repo contains the JavaScript Cloudflare SDK; the server application for recording and displaying data is closed source.
16 |
17 | If you need to instrument your Node.js application, see the [`logfire` package](https://www.npmjs.com/package/logfire).
18 | If you need to instrument your browser application, see the [Logfire Browser package](https://www.npmjs.com/package/@pydantic/logfire-browser).
19 |
20 | ## Basic usage
21 |
22 | See the [cf-worker example](https://github.com/pydantic/logfire-js/tree/main/examples/cf-worker) for a primer.
23 |
24 | ## Contributing
25 |
26 | See [CONTRIBUTING.md](https://github.com/pydantic/logfire-js/blob/main/CONTRIBUTING.md) for development instructions.
27 |
28 | ## License
29 |
30 | MIT
31 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
6 | "target": "es2021",
7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */
8 | "lib": ["es2021"],
9 | /* Specify what JSX code is generated. */
10 | "jsx": "react-jsx",
11 |
12 | /* Specify what module code is generated. */
13 | "module": "es2022",
14 | /* Specify how TypeScript looks up a file from a given module specifier. */
15 | "moduleResolution": "Bundler",
16 | /* Specify type package names to be included without being referenced in a source file. */
17 | "types": ["@cloudflare/workers-types"],
18 | /* Enable importing .json files */
19 | "resolveJsonModule": true,
20 |
21 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
22 | "allowJs": true,
23 | /* Enable error reporting in type-checked JavaScript files. */
24 | "checkJs": false,
25 |
26 | /* Disable emitting files from a compilation. */
27 | "noEmit": true,
28 |
29 | /* Ensure that each file can be safely transpiled without relying on other imports. */
30 | "isolatedModules": true,
31 | /* Allow 'import x from y' when a module doesn't have a default export. */
32 | "allowSyntheticDefaultImports": true,
33 | /* Ensure that casing is correct in imports. */
34 | "forceConsistentCasingInFileNames": true,
35 |
36 | /* Enable all strict type-checking options. */
37 | "strict": true,
38 |
39 | /* Skip type checking all .d.ts files. */
40 | "skipLibCheck": true
41 | },
42 | "exclude": ["test"],
43 | "include": ["worker-configuration.d.ts", "src/**/*.ts"]
44 | }
45 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
6 | "target": "es2021",
7 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */
8 | "lib": ["es2021"],
9 | /* Specify what JSX code is generated. */
10 | "jsx": "react-jsx",
11 |
12 | /* Specify what module code is generated. */
13 | "module": "es2022",
14 | /* Specify how TypeScript looks up a file from a given module specifier. */
15 | "moduleResolution": "Bundler",
16 | /* Specify type package names to be included without being referenced in a source file. */
17 | "types": [
18 | "@cloudflare/workers-types/2023-07-01"
19 | ],
20 | /* Enable importing .json files */
21 | "resolveJsonModule": true,
22 |
23 | /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
24 | "allowJs": true,
25 | /* Enable error reporting in type-checked JavaScript files. */
26 | "checkJs": false,
27 |
28 | /* Disable emitting files from a compilation. */
29 | "noEmit": true,
30 |
31 | /* Ensure that each file can be safely transpiled without relying on other imports. */
32 | "isolatedModules": true,
33 | /* Allow 'import x from y' when a module doesn't have a default export. */
34 | "allowSyntheticDefaultImports": true,
35 | /* Ensure that casing is correct in imports. */
36 | "forceConsistentCasingInFileNames": true,
37 |
38 | /* Enable all strict type-checking options. */
39 | "strict": true,
40 |
41 | /* Skip type checking all .d.ts files. */
42 | "skipLibCheck": true
43 | },
44 | "exclude": ["test"],
45 | "include": ["worker-configuration.d.ts", "src/**/*.ts"]
46 | }
47 |
--------------------------------------------------------------------------------
/packages/logfire-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logfire",
3 | "private": false,
4 | "description": "JavaScript API for Logfire - https://pydantic.dev/logfire",
5 | "author": {
6 | "name": "The Pydantic Team",
7 | "email": "engineering@pydantic.dev",
8 | "url": "https://pydantic.dev"
9 | },
10 | "repository": {
11 | "url": "https://github.com/pydantic/logfire-js",
12 | "directory": "packages/logfire-api"
13 | },
14 | "sideEffects": false,
15 | "homepage": "https://pydantic.dev/logfire",
16 | "license": "MIT",
17 | "publishConfig": {
18 | "access": "public"
19 | },
20 | "keywords": [
21 | "logfire",
22 | "observability",
23 | "opentelemetry",
24 | "tracing",
25 | "profiling",
26 | "stats",
27 | "monitoring"
28 | ],
29 | "version": "0.11.0",
30 | "type": "module",
31 | "main": "./dist/index.cjs",
32 | "module": "./dist/index.js",
33 | "types": "./dist/index.d.ts",
34 | "exports": {
35 | ".": {
36 | "import": {
37 | "types": "./dist/index.d.ts",
38 | "default": "./dist/index.js"
39 | },
40 | "require": {
41 | "types": "./dist/index.d.cts",
42 | "default": "./dist/index.cjs"
43 | }
44 | }
45 | },
46 | "scripts": {
47 | "dev": "vite build",
48 | "build": "vite build",
49 | "lint": "eslint",
50 | "preview": "vite preview",
51 | "typecheck": "tsc",
52 | "prepack": "cp ../../LICENSE .",
53 | "postpack": "rm LICENSE",
54 | "test": "vitest run"
55 | },
56 | "devDependencies": {
57 | "@opentelemetry/api": "^1.9.0",
58 | "@pydantic/logfire-tooling-config": "*",
59 | "eslint": "^9.22.0",
60 | "prettier": "3.5.3",
61 | "typescript": "^5.8.2",
62 | "vite": "^6.2.0",
63 | "vite-plugin-dts": "^4.5.3",
64 | "vitest": "^3.1.1"
65 | },
66 | "peerDependencies": {
67 | "@opentelemetry/api": "^1.9.0"
68 | },
69 | "files": [
70 | "dist",
71 | "LICENSE"
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/packages/logfire-browser/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — JavaScript SDK
2 |
3 | From the team behind [Pydantic Validation](https://pydantic.dev/), **Pydantic Logfire** is an observability platform built on the same belief as our open source library — that the most powerful tools can be easy to use.
4 |
5 | What sets Logfire apart:
6 |
7 | - **Simple and Powerful:** Logfire's dashboard is simple relative to the power it provides, ensuring your entire engineering team will actually use it.
8 | - **SQL:** Query your data using standard SQL — all the control and (for many) nothing new to learn. Using SQL also means you can query your data with existing BI tools and database querying libraries.
9 | - **OpenTelemetry:** Logfire is an opinionated wrapper around OpenTelemetry, allowing you to leverage existing tooling, infrastructure, and instrumentation for many common packages, and enabling support for virtually any language.
10 |
11 | See the [documentation](https://logfire.pydantic.dev/docs/) for more information.
12 |
13 | **Feel free to report issues and ask any questions about Logfire in this repository!**
14 |
15 | This repo contains the JavaScript Browser SDK; the server application for recording and displaying data is closed source.
16 |
17 | If you need to instrument your Node.js application, see the [`logfire` package](https://www.npmjs.com/package/logfire).
18 | If you're instrumenting Cloudflare, see the [Logfire CF workers package](https://www.npmjs.com/package/@pydantic/logfire-cf-workers).
19 |
20 | ## Basic usage
21 |
22 | See the [Logfire Browser docs for a primer](https://logfire.pydantic.dev/docs/integrations/javascript/browser/). Ready to run examples are available in the repository [in vanilla browser](https://github.com/pydantic/logfire-js/tree/main/examples/browser) and [Next.js variants](https://github.com/pydantic/logfire-js/tree/main/examples/nextjs-client-side-instrumentation).
23 |
24 | ## Contributing
25 |
26 | See [CONTRIBUTING.md](https://github.com/pydantic/logfire-js/blob/main/CONTRIBUTING.md) for development instructions.
27 |
28 | ## License
29 |
30 | MIT
31 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/TailWorkerExporter.ts:
--------------------------------------------------------------------------------
1 | import { ExportResult, ExportResultCode } from '@opentelemetry/core'
2 | import { JsonTraceSerializer } from '@opentelemetry/otlp-transformer'
3 | import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'
4 |
5 | import { IExportTraceServiceRequest, IKeyValue } from './OtlpTransformerTypes'
6 |
7 | export class TailWorkerExporter implements SpanExporter {
8 | export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
9 | this._sendSpans(spans, resultCallback)
10 | }
11 |
12 | shutdown(): Promise {
13 | this._sendSpans([])
14 | return Promise.resolve()
15 | }
16 |
17 | private _cleanNullValues(message: IExportTraceServiceRequest) {
18 | if (!message.resourceSpans) {
19 | return message
20 | }
21 | for (const resourceSpan of message.resourceSpans) {
22 | removeEmptyAttributes(resourceSpan.resource)
23 | for (const scopeSpan of resourceSpan.scopeSpans) {
24 | if (scopeSpan.scope) {
25 | removeEmptyAttributes(scopeSpan.scope)
26 | }
27 |
28 | for (const span of scopeSpan.spans ?? []) {
29 | removeEmptyAttributes(span)
30 | }
31 | }
32 | }
33 | return message
34 | }
35 |
36 | private _sendSpans(spans: ReadableSpan[], done?: (result: ExportResult) => void): void {
37 | const bytes = JsonTraceSerializer.serializeRequest(spans)
38 | const jsonString = new TextDecoder().decode(bytes)
39 | const response = JSON.parse(jsonString) as IExportTraceServiceRequest
40 |
41 | const exportMessage = this._cleanNullValues(response)
42 |
43 | console.log(exportMessage)
44 |
45 | return done?.({ code: ExportResultCode.SUCCESS })
46 | }
47 | }
48 |
49 | function removeEmptyAttributes(obj?: { attributes?: IKeyValue[] | undefined }) {
50 | if (obj?.attributes) {
51 | obj.attributes = obj.attributes.filter(nonEmptyAttribute)
52 | }
53 | }
54 |
55 | function nonEmptyAttribute(attribute: IKeyValue) {
56 | return Object.keys(attribute.value).length > 0
57 | }
58 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-cf-workers",
3 | "private": false,
4 | "description": "Cloudflare workers integration for Logfire - https://pydantic.dev/logfire",
5 | "author": {
6 | "name": "The Pydantic Team",
7 | "email": "engineering@pydantic.dev",
8 | "url": "https://pydantic.dev"
9 | },
10 | "repository": {
11 | "url": "git+https://github.com/pydantic/logfire-js.git",
12 | "directory": "packages/logfire-cf-workers"
13 | },
14 | "sideEffects": false,
15 | "homepage": "https://pydantic.dev/logfire",
16 | "license": "MIT",
17 | "publishConfig": {
18 | "access": "public"
19 | },
20 | "keywords": [
21 | "logfire",
22 | "observability",
23 | "opentelemetry",
24 | "tracing",
25 | "profiling",
26 | "stats",
27 | "monitoring"
28 | ],
29 | "version": "0.11.0",
30 | "type": "module",
31 | "main": "./dist/index.cjs",
32 | "module": "./dist/index.js",
33 | "types": "./dist/index.d.ts",
34 | "exports": {
35 | ".": {
36 | "import": {
37 | "types": "./dist/index.d.ts",
38 | "default": "./dist/index.js"
39 | },
40 | "require": {
41 | "types": "./dist/index.d.cts",
42 | "default": "./dist/index.cjs"
43 | }
44 | }
45 | },
46 | "scripts": {
47 | "dev": "vite build",
48 | "build": "vite build",
49 | "lint": "eslint",
50 | "preview": "vite preview",
51 | "typecheck": "tsc",
52 | "prepack": "cp ../../LICENSE .",
53 | "postpack": "rm LICENSE"
54 | },
55 | "dependencies": {
56 | "logfire": "*",
57 | "@pydantic/otel-cf-workers": "^1.0.0-rc.54"
58 | },
59 | "devDependencies": {
60 | "@cloudflare/workers-types": "4.20250311.0",
61 | "@opentelemetry/sdk-trace-base": "^2.0.0",
62 | "@pydantic/logfire-tooling-config": "*",
63 | "eslint": "^9.22.0",
64 | "prettier": "3.5.3",
65 | "typescript": "^5.8.2",
66 | "vite": "^6.2.0",
67 | "vite-plugin-dts": "^4.5.3",
68 | "vitest": "^3.0.8"
69 | },
70 | "files": [
71 | "dist",
72 | "LICENSE"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/packages/logfire-browser/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @pydantic/logfire-browser
2 |
3 | ## 0.12.1
4 |
5 | ### Patch Changes
6 |
7 | - 06fa5d8: Fix logfire-dependency
8 |
9 | ## 0.12.0
10 |
11 | ### Minor Changes
12 |
13 | - 26db714: Use logfire instead of @pydantic/logfire-api
14 |
15 | ## 0.11.0
16 |
17 | ### Minor Changes
18 |
19 | - 00ffa94: Use logfire instead of @pydantic/logfire-api
20 |
21 | ## 0.10.0
22 |
23 | ### Minor Changes
24 |
25 | - 03df4fb: Add default export to packages. Using the default import is equivalent to the star import.
26 |
27 | ### Patch Changes
28 |
29 | - Updated dependencies [03df4fb]
30 | - @pydantic/logfire-api@0.9.0
31 |
32 | ## 0.9.1
33 |
34 | ### Patch Changes
35 |
36 | - 258969c: Update READMEs
37 |
38 | ## 0.9.0
39 |
40 | ### Minor Changes
41 |
42 | - 413ff56: Support logging spans in the console
43 |
44 | ## 0.8.1
45 |
46 | ### Patch Changes
47 |
48 | - 4c22f71: Externalize the context manager, to avoid zone.js patching
49 | - Updated dependencies [4c22f71]
50 | - @pydantic/logfire-api@0.8.1
51 |
52 | ## 0.8.0
53 |
54 | ### Minor Changes
55 |
56 | - f29a18b: Support Zone.js promises
57 |
58 | ### Patch Changes
59 |
60 | - Updated dependencies [f29a18b]
61 | - @pydantic/logfire-api@0.8.0
62 |
63 | ## 0.7.0
64 |
65 | ### Minor Changes
66 |
67 | - 2771f37: Support dynamic headers for the proxy URL
68 |
69 | ## 0.6.0
70 |
71 | ### Minor Changes
72 |
73 | - 763b96a: Improve fetch / click spans
74 |
75 | ## 0.5.0
76 |
77 | ### Minor Changes
78 |
79 | - 71f46db: Auto-close spans opened with logfire.span
80 |
81 | ### Patch Changes
82 |
83 | - Updated dependencies [71f46db]
84 | - @pydantic/logfire-api@0.6.0
85 |
86 | ## 0.4.0
87 |
88 | ### Minor Changes
89 |
90 | - 4d22a69: Support configuration for the trace exporter config
91 |
92 | ## 0.3.1
93 |
94 | ### Patch Changes
95 |
96 | - 9bab4b9: Add the missing dependency
97 |
98 | ## 0.3.0
99 |
100 | ### Minor Changes
101 |
102 | - 088af0d: Support environment configuration
103 |
104 | ## 0.2.0
105 |
106 | ### Minor Changes
107 |
108 | - 478e045: Experimental browser support
109 |
110 | ### Patch Changes
111 |
112 | - 54351e7: Add browser resource attributes
113 | - Updated dependencies [478e045]
114 | - @pydantic/logfire-api@0.5.0
115 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { trace } from '@opentelemetry/api'
2 | import { beforeEach, describe, expect, test, vi } from 'vitest'
3 |
4 | import {
5 | ATTRIBUTES_LEVEL_KEY,
6 | ATTRIBUTES_MESSAGE_KEY,
7 | ATTRIBUTES_MESSAGE_TEMPLATE_KEY,
8 | ATTRIBUTES_SPAN_TYPE_KEY,
9 | ATTRIBUTES_TAGS_KEY,
10 | } from './constants'
11 | import { info } from './index'
12 |
13 | vi.mock('@opentelemetry/api', () => {
14 | const spanMock = {
15 | end: vi.fn(),
16 | setAttribute: vi.fn(),
17 | setStatus: vi.fn(),
18 | }
19 |
20 | const tracerMock = {
21 | startSpan: vi.fn(() => spanMock),
22 | }
23 |
24 | return {
25 | context: {
26 | active: vi.fn(),
27 | },
28 | trace: {
29 | getTracer: vi.fn(() => tracerMock),
30 | },
31 | }
32 | })
33 |
34 | describe('info', () => {
35 | beforeEach(() => {
36 | vi.clearAllMocks()
37 | })
38 | test('formats the message with the passed attributes', () => {
39 | info('aha {i}', { i: 1 })
40 | const tracer = trace.getTracer('logfire')
41 |
42 | // eslint-disable-next-line @typescript-eslint/unbound-method
43 | expect(tracer.startSpan).toBeCalledWith(
44 | 'aha {i}',
45 | {
46 | attributes: {
47 | [ATTRIBUTES_LEVEL_KEY]: 9,
48 | [ATTRIBUTES_MESSAGE_KEY]: 'aha 1',
49 | [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: 'aha {i}',
50 | [ATTRIBUTES_SPAN_TYPE_KEY]: 'log',
51 | [ATTRIBUTES_TAGS_KEY]: [],
52 | i: 1,
53 | },
54 | },
55 | undefined
56 | )
57 | })
58 |
59 | test('adds scrubbing details', () => {
60 | info('aha {i}', { i: 1, password: 'hunter' })
61 | const tracer = trace.getTracer('logfire')
62 |
63 | // eslint-disable-next-line @typescript-eslint/unbound-method
64 | expect(tracer.startSpan).toBeCalledWith(
65 | 'aha {i}',
66 | {
67 | attributes: {
68 | [ATTRIBUTES_LEVEL_KEY]: 9,
69 | [ATTRIBUTES_MESSAGE_KEY]: 'aha 1',
70 | [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: 'aha {i}',
71 | [ATTRIBUTES_SPAN_TYPE_KEY]: 'log',
72 | [ATTRIBUTES_TAGS_KEY]: [],
73 | i: 1,
74 | 'logfire.json_schema': '{"properties":{"logfire.scrubbed":{"type":"array"}},"type":"object"}',
75 | 'logfire.scrubbed': '[{"matched_substring":"password","path":["password"]}]',
76 | password: "[Scrubbed due to 'password']",
77 | },
78 | },
79 | undefined
80 | )
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/packages/logfire-browser/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-browser",
3 | "description": "JavaScript Browser SDK for Logfire - https://pydantic.dev/logfire",
4 | "author": {
5 | "name": "The Pydantic Team",
6 | "email": "engineering@pydantic.dev",
7 | "url": "https://pydantic.dev"
8 | },
9 | "repository": {
10 | "url": "https://github.com/pydantic/logfire-js",
11 | "directory": "packages/logfire-browser"
12 | },
13 | "sideEffects": false,
14 | "homepage": "https://pydantic.dev/logfire",
15 | "license": "MIT",
16 | "publishConfig": {
17 | "access": "public"
18 | },
19 | "keywords": [
20 | "logfire",
21 | "observability",
22 | "opentelemetry",
23 | "tracing",
24 | "profiling",
25 | "stats",
26 | "monitoring"
27 | ],
28 | "version": "0.12.1",
29 | "type": "module",
30 | "main": "./dist/index.cjs",
31 | "module": "./dist/index.js",
32 | "types": "./dist/index.d.ts",
33 | "exports": {
34 | ".": {
35 | "import": {
36 | "types": "./dist/index.d.ts",
37 | "default": "./dist/index.js"
38 | },
39 | "require": {
40 | "types": "./dist/index.d.cts",
41 | "default": "./dist/index.cjs"
42 | }
43 | }
44 | },
45 | "scripts": {
46 | "dev": "vite build",
47 | "build": "vite build",
48 | "lint": "eslint",
49 | "preview": "vite preview",
50 | "typecheck": "tsc",
51 | "prepack": "cp ../../LICENSE .",
52 | "postpack": "rm LICENSE",
53 | "test": "vitest run --passWithNoTests"
54 | },
55 | "dependencies": {
56 | "@opentelemetry/api": "^1.9.0",
57 | "@opentelemetry/core": "^2.0.1",
58 | "@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
59 | "@opentelemetry/instrumentation": "^0.202.0",
60 | "@opentelemetry/otlp-exporter-base": "^0.203.0",
61 | "@opentelemetry/resources": "^2.0.1",
62 | "logfire": "^0.11.0"
63 | },
64 | "peerDependencies": {
65 | "@opentelemetry/sdk-trace-web": "^2.0.1",
66 | "@opentelemetry/semantic-conventions": "^1.34.0"
67 | },
68 | "devDependencies": {
69 | "@opentelemetry/sdk-trace-web": "^2.0.1",
70 | "@opentelemetry/semantic-conventions": "^1.34.0",
71 | "eslint": "^9.22.0",
72 | "prettier": "3.5.3",
73 | "typescript": "^5.8.2",
74 | "user-agent-data-types": "^0.4.2",
75 | "vite": "^6.2.0",
76 | "vite-plugin-dts": "^4.5.3",
77 | "vitest": "^3.1.1"
78 | },
79 | "files": [
80 | "dist",
81 | "LICENSE"
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/serializeAttributes.ts:
--------------------------------------------------------------------------------
1 | import { logfireApiConfig, ScrubbedNote } from '.'
2 | import { ATTRIBUTES_SCRUBBED_KEY, ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY, JSON_NULL_FIELDS_KEY, JSON_SCHEMA_KEY } from './constants'
3 |
4 | export type AttributeValue = boolean | number | string | string[]
5 |
6 | export type RawAttributes = Record
7 |
8 | interface JSONSchema {
9 | properties: Record<
10 | string,
11 | {
12 | type: 'array' | 'object'
13 | }
14 | >
15 | type: 'object'
16 | }
17 |
18 | type SerializedAttributes = Record
19 |
20 | export function serializeAttributes(attributes: RawAttributes): SerializedAttributes {
21 | const scrubber = logfireApiConfig.scrubber
22 | const alreadyScubbed = ATTRIBUTES_SPAN_TYPE_KEY in attributes
23 | const [scrubbedAttributes, scrubNotes] = alreadyScubbed ? [attributes, []] : scrubber.scrubValue([], attributes)
24 |
25 | const result: SerializedAttributes = {}
26 | const nullArgs: string[] = []
27 | const schema: JSONSchema = { properties: {}, type: 'object' }
28 |
29 | if (scrubNotes.length > 0) {
30 | if (ATTRIBUTES_SCRUBBED_KEY in scrubbedAttributes) {
31 | ;(scrubbedAttributes[ATTRIBUTES_SCRUBBED_KEY] as ScrubbedNote[]).push(...scrubNotes)
32 | } else {
33 | scrubbedAttributes[ATTRIBUTES_SCRUBBED_KEY] = scrubNotes
34 | }
35 | }
36 | for (const [key, value] of Object.entries(scrubbedAttributes)) {
37 | // we don't want to serialize the tags
38 | if (key === ATTRIBUTES_TAGS_KEY) {
39 | result[key] = value as string[]
40 | continue
41 | }
42 |
43 | if (value === null || value === undefined) {
44 | nullArgs.push(key)
45 | } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
46 | result[key] = value
47 | } else if (value instanceof Date) {
48 | result[key] = value.toISOString()
49 | } else if (Array.isArray(value)) {
50 | schema.properties[key] = {
51 | type: 'array',
52 | }
53 | result[key] = JSON.stringify(value)
54 | } else {
55 | schema.properties[key] = {
56 | type: 'object',
57 | }
58 |
59 | result[key] = JSON.stringify(value)
60 | }
61 | }
62 | if (nullArgs.length > 0) {
63 | result[JSON_NULL_FIELDS_KEY] = nullArgs
64 | }
65 | if (Object.keys(schema.properties).length > 0) {
66 | result[JSON_SCHEMA_KEY] = JSON.stringify(schema)
67 | }
68 | return result
69 | }
70 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/LogfireConsoleSpanExporter.ts:
--------------------------------------------------------------------------------
1 | import { ExportResult, ExportResultCode, hrTimeToMicroseconds } from '@opentelemetry/core'
2 | import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'
3 | import pc from 'picocolors'
4 |
5 | const LevelLabels = {
6 | 1: 'trace',
7 | 5: 'debug',
8 | 9: 'info',
9 | 10: 'notice',
10 | 13: 'warning',
11 | 17: 'error',
12 | 21: 'fatal',
13 | } as const
14 |
15 | const ColorMap = {
16 | debug: pc.blue,
17 | error: pc.red,
18 | fatal: pc.magenta,
19 | info: pc.cyan,
20 | notice: pc.green,
21 | trace: pc.gray,
22 | warning: pc.yellow,
23 | }
24 |
25 | export class LogfireConsoleSpanExporter implements SpanExporter {
26 | export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
27 | this.sendSpans(spans, resultCallback)
28 | }
29 |
30 | forceFlush(): Promise {
31 | return Promise.resolve()
32 | }
33 | shutdown(): Promise {
34 | this.sendSpans([])
35 | return this.forceFlush()
36 | }
37 |
38 | /**
39 | * converts span info into more readable format
40 | * @param span
41 | */
42 | private exportInfo(span: ReadableSpan) {
43 | return {
44 | attributes: span.attributes,
45 | duration: hrTimeToMicroseconds(span.duration),
46 | events: span.events,
47 | id: span.spanContext().spanId,
48 | instrumentationScope: span.instrumentationScope,
49 | kind: span.kind,
50 | links: span.links,
51 | name: span.name,
52 | parentSpanContext: span.parentSpanContext,
53 | resource: {
54 | attributes: span.resource.attributes,
55 | },
56 | status: span.status,
57 | timestamp: hrTimeToMicroseconds(span.startTime),
58 | traceId: span.spanContext().traceId,
59 | traceState: span.spanContext().traceState?.serialize(),
60 | }
61 | }
62 |
63 | private sendSpans(spans: ReadableSpan[], done?: (result: ExportResult) => void): void {
64 | for (const span of spans) {
65 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
66 | const type = LevelLabels[span.attributes['logfire.level_num'] as keyof typeof LevelLabels] ?? 'info'
67 |
68 | const { attributes, name, ...rest } = this.exportInfo(span)
69 | console.log(`${pc.bgMagentaBright('Logfire')} ${ColorMap[type](type)} ${name}`)
70 | console.dir(attributes)
71 | console.dir(rest)
72 | }
73 | if (done) {
74 | done({ code: ExportResultCode.SUCCESS })
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/LogfireCloudflareConsoleSpanExporter.ts:
--------------------------------------------------------------------------------
1 | import { ExportResult, ExportResultCode, hrTimeToMicroseconds } from '@opentelemetry/core'
2 | import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'
3 |
4 | const LevelLabels = {
5 | 1: 'trace',
6 | 5: 'debug',
7 | 9: 'info',
8 | 10: 'notice',
9 | 13: 'warning',
10 | 17: 'error',
11 | 21: 'fatal',
12 | } as const
13 |
14 | /**
15 | * Prints spans in the terminal, using the respective color sequences.
16 | */
17 | export class LogfireCloudflareConsoleSpanExporter implements SpanExporter {
18 | export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
19 | this.sendSpans(spans, resultCallback)
20 | }
21 |
22 | forceFlush(): Promise {
23 | return Promise.resolve()
24 | }
25 | shutdown(): Promise {
26 | this.sendSpans([])
27 | return this.forceFlush()
28 | }
29 |
30 | /**
31 | * converts span info into more readable format
32 | * @param span
33 | */
34 | private exportInfo(span: ReadableSpan) {
35 | return {
36 | attributes: span.attributes,
37 | duration: hrTimeToMicroseconds(span.duration),
38 | events: span.events,
39 | id: span.spanContext().spanId,
40 | instrumentationScope: span.instrumentationScope,
41 | kind: span.kind,
42 | links: span.links,
43 | name: span.name,
44 | parentSpanContext: span.parentSpanContext,
45 | resource: {
46 | attributes: span.resource.attributes,
47 | },
48 | status: span.status,
49 | timestamp: hrTimeToMicroseconds(span.startTime),
50 | traceId: span.spanContext().traceId,
51 | traceState: span.spanContext().traceState?.serialize(),
52 | }
53 | }
54 |
55 | private sendSpans(spans: ReadableSpan[], done?: (result: ExportResult) => void): void {
56 | for (const span of spans) {
57 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
58 | const type = LevelLabels[span.attributes['logfire.level_num'] as keyof typeof LevelLabels] ?? 'info'
59 |
60 | const { attributes, name, ...rest } = this.exportInfo(span)
61 | console.log(`Logfire: ${type} >> ${name}`)
62 | console.log('Attributes:')
63 | console.log(JSON.stringify(attributes, null, 2))
64 | console.log('---')
65 | console.log('Span details:')
66 | console.log(JSON.stringify(rest, null, 2))
67 | console.log('---')
68 | }
69 | if (done) {
70 | done({ code: ExportResultCode.SUCCESS })
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/traceExporter.ts:
--------------------------------------------------------------------------------
1 | import { Context } from '@opentelemetry/api'
2 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
3 | import { BatchSpanProcessor, ReadableSpan, SimpleSpanProcessor, Span, SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base'
4 |
5 | import { logfireConfig } from './logfireConfig'
6 | import { LogfireConsoleSpanExporter } from './LogfireConsoleSpanExporter'
7 | import { VoidTraceExporter } from './VoidTraceExporter'
8 |
9 | export function logfireSpanProcessor(enableConsole: boolean | undefined) {
10 | return new LogfireSpanProcessor(new BatchSpanProcessor(traceExporter()), Boolean(enableConsole))
11 | }
12 |
13 | /**
14 | * returns an OTLPTraceExporter instance pointing to the Logfire endpoint.
15 | */
16 | export function traceExporter(): SpanExporter {
17 | if (!logfireConfig.sendToLogfire) {
18 | return new VoidTraceExporter()
19 | }
20 |
21 | const token = logfireConfig.token
22 | if (!token) {
23 | // TODO: what should be done here? We're forcing sending to logfire, but we don't have a token
24 | throw new Error('Logfire token is required')
25 | }
26 |
27 | return new OTLPTraceExporter({
28 | headers: logfireConfig.authorizationHeaders,
29 | url: logfireConfig.traceExporterUrl,
30 | })
31 | }
32 |
33 | class LogfireSpanProcessor implements SpanProcessor {
34 | private console?: SpanProcessor
35 | private wrapped: SpanProcessor
36 |
37 | constructor(wrapped: SpanProcessor, enableConsole: boolean) {
38 | if (enableConsole) {
39 | this.console = new SimpleSpanProcessor(new LogfireConsoleSpanExporter())
40 | }
41 | this.wrapped = wrapped
42 | }
43 |
44 | async forceFlush(): Promise {
45 | await this.console?.forceFlush()
46 | return this.wrapped.forceFlush()
47 | }
48 |
49 | onEnd(span: ReadableSpan): void {
50 | this.console?.onEnd(span)
51 | // Note: this is too late for the regular node instrumentation. The opentelemetry API rejects the non-primitive attribute values.
52 | // Instead, the serialization happens at the `logfire.span, logfire.startSpan`, etc.
53 | // Object.assign(span.attributes, serializeAttributes(span.attributes))
54 | this.wrapped.onEnd(span)
55 | }
56 |
57 | onStart(span: Span, parentContext: Context): void {
58 | this.console?.onStart(span, parentContext)
59 | this.wrapped.onStart(span, parentContext)
60 | }
61 |
62 | async shutdown(): Promise {
63 | await this.console?.shutdown()
64 | return this.wrapped.shutdown()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/packages/logfire-api/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @pydantic/logfire-api
2 |
3 | ## 0.11.0
4 |
5 | ### Minor Changes
6 |
7 | - 28eb056: BREAKING CHANGE: Package renamed from `@pydantic/logfire-api` to `logfire`.
8 |
9 | This change makes the core API package easier to use with a simpler, unscoped name.
10 |
11 | **Migration Guide**:
12 |
13 | - Update package.json: Change `"@pydantic/logfire-api"` to `"logfire"`
14 | - Update imports: Change `from '@pydantic/logfire-api'` to `from 'logfire'`
15 | - Run `npm install` to update lockfiles
16 |
17 | The package functionality remains identical. This is purely a naming change.
18 |
19 | **Why this change?**
20 | The core API package is used across all runtimes (Node, browser, Cloudflare Workers) and deserves the simpler package name. The Node.js-specific SDK with auto-instrumentation is now `@pydantic/logfire-node`.
21 |
22 | ## 0.9.0
23 |
24 | ### Minor Changes
25 |
26 | - 03df4fb: Add default export to packages. Using the default import is equivalent to the star import.
27 |
28 | ## 0.8.2
29 |
30 | ### Patch Changes
31 |
32 | - 8c57b16: Do not format span_name
33 |
34 | ## 0.8.1
35 |
36 | ### Patch Changes
37 |
38 | - 4c22f71: Externalize the context manager, to avoid zone.js patching
39 |
40 | ## 0.8.0
41 |
42 | ### Minor Changes
43 |
44 | - f29a18b: Support Zone.js promises
45 |
46 | ## 0.7.0
47 |
48 | ### Minor Changes
49 |
50 | - 2f2f859: Improve nested span API
51 |
52 | - Add convenient 2 argument overload for `span`.
53 | - Support `parentSpan` option to nest spans manually.
54 |
55 | ## 0.6.1
56 |
57 | ### Patch Changes
58 |
59 | - 421b666: Fix async parent span timing
60 |
61 | ## 0.6.0
62 |
63 | ### Minor Changes
64 |
65 | - 71f46db: Auto-close spans opened with logfire.span
66 |
67 | ## 0.5.0
68 |
69 | ### Minor Changes
70 |
71 | - 478e045: Experimental browser support
72 |
73 | ## 0.4.2
74 |
75 | ### Patch Changes
76 |
77 | - fac89ec: logfire.reportError - documentation and setting correct span type
78 | - fac89ec: Document and slightly enhance the `reportError` function.
79 |
80 | ## 0.4.1
81 |
82 | ### Patch Changes
83 |
84 | - cd2ac40: Fix attribute serialization
85 |
86 | ## 0.4.0
87 |
88 | ### Minor Changes
89 |
90 | - dc0a537: Support for EU tokens. Support span message formatting.
91 |
92 | ## 0.3.0
93 |
94 | ### Minor Changes
95 |
96 | - 6fa1410: API updates, fixes for span kind
97 |
98 | ## 0.2.1
99 |
100 | ### Patch Changes
101 |
102 | - 838ba5d: Fix packages publish settings.
103 |
104 | ## 0.2.0
105 |
106 | ### Minor Changes
107 |
108 | - 0f0ce8f: Initial release.
109 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @pydantic/logfire-cf-workers
2 |
3 | ## 0.11.0
4 |
5 | ### Minor Changes
6 |
7 | - 26db714: Use logfire instead of @pydantic/logfire-api
8 |
9 | ## 0.10.0
10 |
11 | ### Minor Changes
12 |
13 | - 00ffa94: Use logfire instead of @pydantic/logfire-api
14 |
15 | ## 0.9.0
16 |
17 | ### Minor Changes
18 |
19 | - 03df4fb: Add default export to packages. Using the default import is equivalent to the star import.
20 |
21 | ### Patch Changes
22 |
23 | - Updated dependencies [03df4fb]
24 | - @pydantic/logfire-api@0.9.0
25 |
26 | ## 0.8.2
27 |
28 | ### Patch Changes
29 |
30 | - 4ec3564: Diagnostic host message
31 |
32 | ## 0.8.1
33 |
34 | ### Patch Changes
35 |
36 | - 258969c: Update READMEs
37 |
38 | ## 0.8.0
39 |
40 | ### Minor Changes
41 |
42 | - 3203828: Support additional span processors for cf workers
43 |
44 | ## 0.7.0
45 |
46 | ### Minor Changes
47 |
48 | - 5e54d9c: Allow disabling scrubbing
49 | - 0787869: Support console logging of spans for CF workers
50 |
51 | ## 0.6.0
52 |
53 | ### Minor Changes
54 |
55 | - 71f46db: Auto-close spans opened with logfire.span
56 |
57 | ### Patch Changes
58 |
59 | - Updated dependencies [71f46db]
60 | - @pydantic/logfire-api@0.6.0
61 |
62 | ## 0.5.0
63 |
64 | ### Minor Changes
65 |
66 | - 478e045: Experimental browser support
67 |
68 | ### Patch Changes
69 |
70 | - Updated dependencies [478e045]
71 | - @pydantic/logfire-api@0.5.0
72 |
73 | ## 0.4.4
74 |
75 | ### Patch Changes
76 |
77 | - df4ac70: Support environment for Cloudflare workers
78 |
79 | ## 0.4.3
80 |
81 | ### Patch Changes
82 |
83 | - 17dbddd: Re-export instrument function
84 |
85 | ## 0.4.2
86 |
87 | ### Patch Changes
88 |
89 | - b59e803: Bump to latest otel-cf-workers, fixes span nesting and adds header capturing
90 |
91 | ## 0.4.1
92 |
93 | ### Patch Changes
94 |
95 | - af427c5: Support for tail worker trace exporting
96 |
97 | ## 0.4.0
98 |
99 | ### Minor Changes
100 |
101 | - dc0a537: Support for EU tokens. Support span message formatting.
102 |
103 | ### Patch Changes
104 |
105 | - Updated dependencies [dc0a537]
106 | - @pydantic/logfire-api@0.4.0
107 |
108 | ## 0.3.0
109 |
110 | ### Minor Changes
111 |
112 | - 6fa1410: API updates, fixes for span kind
113 |
114 | ### Patch Changes
115 |
116 | - Updated dependencies [6fa1410]
117 | - @pydantic/logfire-api@0.3.0
118 |
119 | ## 0.2.2
120 |
121 | ### Patch Changes
122 |
123 | - 11c5ac2: Embed microlabs as a dependency
124 |
125 | ## 0.2.1
126 |
127 | ### Patch Changes
128 |
129 | - 838ba5d: Fix packages publish settings.
130 |
131 | ## 0.2.0
132 |
133 | ### Minor Changes
134 |
135 | - 0f0ce8f: Initial release.
136 |
--------------------------------------------------------------------------------
/packages/logfire-node/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — JavaScript SDK
2 |
3 | From the team behind [Pydantic Validation](https://pydantic.dev/), **Pydantic Logfire** is an observability platform built on the same belief as our open source library — that the most powerful tools can be easy to use.
4 |
5 | What sets Logfire apart:
6 |
7 | - **Simple and Powerful:** Logfire's dashboard is simple relative to the power it provides, ensuring your entire engineering team will actually use it.
8 | - **SQL:** Query your data using standard SQL — all the control and (for many) nothing new to learn. Using SQL also means you can query your data with existing BI tools and database querying libraries.
9 | - **OpenTelemetry:** Logfire is an opinionated wrapper around OpenTelemetry, allowing you to leverage existing tooling, infrastructure, and instrumentation for many common packages, and enabling support for virtually any language.
10 |
11 | See the [documentation](https://logfire.pydantic.dev/docs/) for more information.
12 |
13 | **Feel free to report issues and ask any questions about Logfire in this repository!**
14 |
15 | This repo contains the JavaScript Node.js SDK; the server application for recording and displaying data is closed source.
16 |
17 | If you need to instrument your browser application, see the [Logfire Browser package](https://www.npmjs.com/package/@pydantic/logfire-browser).
18 | If you're instrumenting Cloudflare, see the [Logfire CF workers package](https://www.npmjs.com/package/@pydantic/logfire-cf-workers).
19 |
20 | ## Basic usage
21 |
22 | Using Logfire from your Node.js script is as simple as
23 | [getting a write token](https://logfire.pydantic.dev/docs/how-to-guides/create-write-tokens/),
24 | installing the package, calling configure, and using the provided API. Let's
25 | create an empty project:
26 |
27 | ```sh
28 | mkdir test-logfire-js
29 | cd test-logfire-js
30 | npm init -y es6 # creates package.json with `type: module`
31 | npm install @pydantic/logfire-node
32 | ```
33 |
34 | Then, create the following `hello.js` script in the directory:
35 |
36 | ```js
37 | import * as logfire from '@pydantic/logfire-node'
38 |
39 | logfire.configure({
40 | token: 'my-write-token', // replace with your write token
41 | serviceName: 'example-node-script',
42 | serviceVersion: '1.0.0',
43 | })
44 |
45 | logfire.info(
46 | 'Hello from Node.js',
47 | {
48 | 'attribute-key': 'attribute-value',
49 | },
50 | {
51 | tags: ['example', 'example2'],
52 | }
53 | )
54 | ```
55 |
56 | Run the script with `node hello.js`, and you should see the span being logged in
57 | the live view of your Logfire project.
58 |
59 | ## Contributing
60 |
61 | See [CONTRIBUTING.md](https://github.com/pydantic/logfire-js/blob/main/CONTRIBUTING.md) for development instructions.
62 |
63 | ## License
64 |
65 | MIT
66 |
--------------------------------------------------------------------------------
/examples/cf-worker/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 |
3 | logs
4 | _.log
5 | npm-debug.log_
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 |
58 | web_modules/
59 |
60 | # TypeScript cache
61 |
62 | \*.tsbuildinfo
63 |
64 | # Optional npm cache directory
65 |
66 | .npm
67 |
68 | # Optional eslint cache
69 |
70 | .eslintcache
71 |
72 | # Optional stylelint cache
73 |
74 | .stylelintcache
75 |
76 | # Microbundle cache
77 |
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 |
85 | .node_repl_history
86 |
87 | # Output of 'npm pack'
88 |
89 | \*.tgz
90 |
91 | # Yarn Integrity file
92 |
93 | .yarn-integrity
94 |
95 | # dotenv environment variable files
96 |
97 | .env
98 | .env.development.local
99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 |
103 | # parcel-bundler cache (https://parceljs.org/)
104 |
105 | .cache
106 | .parcel-cache
107 |
108 | # Next.js build output
109 |
110 | .next
111 | out
112 |
113 | # Nuxt.js build / generate output
114 |
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 |
120 | .cache/
121 |
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 |
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 |
126 | # public
127 |
128 | # vuepress build output
129 |
130 | .vuepress/dist
131 |
132 | # vuepress v2.x temp and cache directory
133 |
134 | .temp
135 | .cache
136 |
137 | # Docusaurus cache and generated files
138 |
139 | .docusaurus
140 |
141 | # Serverless directories
142 |
143 | .serverless/
144 |
145 | # FuseBox cache
146 |
147 | .fusebox/
148 |
149 | # DynamoDB Local files
150 |
151 | .dynamodb/
152 |
153 | # TernJS port file
154 |
155 | .tern-port
156 |
157 | # Stores VSCode versions used for testing VSCode extensions
158 |
159 | .vscode-test
160 |
161 | # yarn v2
162 |
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 |
169 | # wrangler project
170 |
171 | .dev.vars
172 | .wrangler/
173 |
--------------------------------------------------------------------------------
/examples/cf-producer-worker/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 |
3 | logs
4 | _.log
5 | npm-debug.log_
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 |
58 | web_modules/
59 |
60 | # TypeScript cache
61 |
62 | \*.tsbuildinfo
63 |
64 | # Optional npm cache directory
65 |
66 | .npm
67 |
68 | # Optional eslint cache
69 |
70 | .eslintcache
71 |
72 | # Optional stylelint cache
73 |
74 | .stylelintcache
75 |
76 | # Microbundle cache
77 |
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 |
85 | .node_repl_history
86 |
87 | # Output of 'npm pack'
88 |
89 | \*.tgz
90 |
91 | # Yarn Integrity file
92 |
93 | .yarn-integrity
94 |
95 | # dotenv environment variable files
96 |
97 | .env
98 | .env.development.local
99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 |
103 | # parcel-bundler cache (https://parceljs.org/)
104 |
105 | .cache
106 | .parcel-cache
107 |
108 | # Next.js build output
109 |
110 | .next
111 | out
112 |
113 | # Nuxt.js build / generate output
114 |
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 |
120 | .cache/
121 |
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 |
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 |
126 | # public
127 |
128 | # vuepress build output
129 |
130 | .vuepress/dist
131 |
132 | # vuepress v2.x temp and cache directory
133 |
134 | .temp
135 | .cache
136 |
137 | # Docusaurus cache and generated files
138 |
139 | .docusaurus
140 |
141 | # Serverless directories
142 |
143 | .serverless/
144 |
145 | # FuseBox cache
146 |
147 | .fusebox/
148 |
149 | # DynamoDB Local files
150 |
151 | .dynamodb/
152 |
153 | # TernJS port file
154 |
155 | .tern-port
156 |
157 | # Stores VSCode versions used for testing VSCode extensions
158 |
159 | .vscode-test
160 |
161 | # yarn v2
162 |
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 |
169 | # wrangler project
170 |
171 | .dev.vars
172 | .wrangler/
173 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 |
3 | logs
4 | _.log
5 | npm-debug.log_
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 |
58 | web_modules/
59 |
60 | # TypeScript cache
61 |
62 | \*.tsbuildinfo
63 |
64 | # Optional npm cache directory
65 |
66 | .npm
67 |
68 | # Optional eslint cache
69 |
70 | .eslintcache
71 |
72 | # Optional stylelint cache
73 |
74 | .stylelintcache
75 |
76 | # Microbundle cache
77 |
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 |
85 | .node_repl_history
86 |
87 | # Output of 'npm pack'
88 |
89 | \*.tgz
90 |
91 | # Yarn Integrity file
92 |
93 | .yarn-integrity
94 |
95 | # dotenv environment variable files
96 |
97 | .env
98 | .env.development.local
99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 |
103 | # parcel-bundler cache (https://parceljs.org/)
104 |
105 | .cache
106 | .parcel-cache
107 |
108 | # Next.js build output
109 |
110 | .next
111 | out
112 |
113 | # Nuxt.js build / generate output
114 |
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 |
120 | .cache/
121 |
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 |
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 |
126 | # public
127 |
128 | # vuepress build output
129 |
130 | .vuepress/dist
131 |
132 | # vuepress v2.x temp and cache directory
133 |
134 | .temp
135 | .cache
136 |
137 | # Docusaurus cache and generated files
138 |
139 | .docusaurus
140 |
141 | # Serverless directories
142 |
143 | .serverless/
144 |
145 | # FuseBox cache
146 |
147 | .fusebox/
148 |
149 | # DynamoDB Local files
150 |
151 | .dynamodb/
152 |
153 | # TernJS port file
154 |
155 | .tern-port
156 |
157 | # Stores VSCode versions used for testing VSCode extensions
158 |
159 | .vscode-test
160 |
161 | # yarn v2
162 |
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 |
169 | # wrangler project
170 |
171 | .dev.vars
172 | .wrangler/
173 |
--------------------------------------------------------------------------------
/packages/logfire-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-node",
3 | "description": "Node.js SDK for Logfire with automatic instrumentation - https://pydantic.dev/logfire",
4 | "author": {
5 | "name": "The Pydantic Team",
6 | "email": "engineering@pydantic.dev",
7 | "url": "https://pydantic.dev"
8 | },
9 | "repository": {
10 | "url": "git+https://github.com/pydantic/logfire-js.git",
11 | "directory": "packages/logfire-node"
12 | },
13 | "sideEffects": false,
14 | "homepage": "https://pydantic.dev/logfire",
15 | "license": "MIT",
16 | "publishConfig": {
17 | "access": "public"
18 | },
19 | "keywords": [
20 | "logfire",
21 | "observability",
22 | "opentelemetry",
23 | "tracing",
24 | "profiling",
25 | "stats",
26 | "monitoring"
27 | ],
28 | "version": "0.11.2",
29 | "type": "module",
30 | "main": "./dist/index.cjs",
31 | "module": "./dist/index.js",
32 | "types": "./dist/index.d.ts",
33 | "exports": {
34 | ".": {
35 | "import": {
36 | "types": "./dist/index.d.ts",
37 | "default": "./dist/index.js"
38 | },
39 | "require": {
40 | "types": "./dist/index.d.cts",
41 | "default": "./dist/index.cjs"
42 | }
43 | }
44 | },
45 | "scripts": {
46 | "dev": "vite build",
47 | "build": "vite build",
48 | "lint": "eslint",
49 | "preview": "vite preview",
50 | "typecheck": "tsc",
51 | "prepack": "cp ../../LICENSE .",
52 | "postpack": "rm LICENSE",
53 | "test": "vitest run --passWithNoTests"
54 | },
55 | "dependencies": {
56 | "logfire": "*",
57 | "picocolors": "^1.1.1"
58 | },
59 | "devDependencies": {
60 | "@opentelemetry/api": "^1.9.0",
61 | "@opentelemetry/auto-instrumentations-node": "^0.57.0",
62 | "@opentelemetry/context-async-hooks": "^2.0.0",
63 | "@opentelemetry/core": "^2.0.0",
64 | "@opentelemetry/exporter-metrics-otlp-proto": "^0.200.0",
65 | "@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
66 | "@opentelemetry/instrumentation": "^0.202.0",
67 | "@opentelemetry/resources": "^2.0.0",
68 | "@opentelemetry/sdk-metrics": "^2.0.0",
69 | "@opentelemetry/sdk-node": "^0.200.0",
70 | "@opentelemetry/sdk-trace-base": "^2.0.0",
71 | "@pydantic/logfire-tooling-config": "*",
72 | "eslint": "^9.22.0",
73 | "prettier": "3.5.3",
74 | "typescript": "^5.8.2",
75 | "vite": "^6.2.0",
76 | "vite-plugin-dts": "^4.5.3",
77 | "vitest": "^3.1.1"
78 | },
79 | "peerDependencies": {
80 | "@opentelemetry/api": "^1.9.0",
81 | "@opentelemetry/auto-instrumentations-node": "^0.57.0",
82 | "@opentelemetry/context-async-hooks": "^2.0.0",
83 | "@opentelemetry/core": "^2.0.0",
84 | "@opentelemetry/exporter-metrics-otlp-proto": "^0.200.0",
85 | "@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
86 | "@opentelemetry/instrumentation": "^0.202.0",
87 | "@opentelemetry/resources": "^2.0.0",
88 | "@opentelemetry/sdk-metrics": "^2.0.0",
89 | "@opentelemetry/sdk-node": "^0.200.0",
90 | "@opentelemetry/sdk-trace-base": "^2.0.0"
91 | },
92 | "files": [
93 | "dist",
94 | "LICENSE"
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/page.module.css:
--------------------------------------------------------------------------------
1 | .page {
2 | --gray-rgb: 0, 0, 0;
3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
5 |
6 | --button-primary-hover: #383838;
7 | --button-secondary-hover: #f2f2f2;
8 |
9 | display: grid;
10 | grid-template-rows: 20px 1fr 20px;
11 | align-items: center;
12 | justify-items: center;
13 | min-height: 100svh;
14 | padding: 80px;
15 | gap: 64px;
16 | font-family: var(--font-geist-sans);
17 | }
18 |
19 | @media (prefers-color-scheme: dark) {
20 | .page {
21 | --gray-rgb: 255, 255, 255;
22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
24 |
25 | --button-primary-hover: #ccc;
26 | --button-secondary-hover: #1a1a1a;
27 | }
28 | }
29 |
30 | .main {
31 | display: flex;
32 | flex-direction: column;
33 | gap: 32px;
34 | grid-row-start: 2;
35 | }
36 |
37 | .main ol {
38 | font-family: var(--font-geist-mono);
39 | padding-left: 0;
40 | margin: 0;
41 | font-size: 14px;
42 | line-height: 24px;
43 | letter-spacing: -0.01em;
44 | list-style-position: inside;
45 | }
46 |
47 | .main li:not(:last-of-type) {
48 | margin-bottom: 8px;
49 | }
50 |
51 | .main code {
52 | font-family: inherit;
53 | background: var(--gray-alpha-100);
54 | padding: 2px 4px;
55 | border-radius: 4px;
56 | font-weight: 600;
57 | }
58 |
59 | .ctas {
60 | display: flex;
61 | gap: 16px;
62 | }
63 |
64 | .ctas a {
65 | appearance: none;
66 | border-radius: 128px;
67 | height: 48px;
68 | padding: 0 20px;
69 | border: none;
70 | border: 1px solid transparent;
71 | transition:
72 | background 0.2s,
73 | color 0.2s,
74 | border-color 0.2s;
75 | cursor: pointer;
76 | display: flex;
77 | align-items: center;
78 | justify-content: center;
79 | font-size: 16px;
80 | line-height: 20px;
81 | font-weight: 500;
82 | }
83 |
84 | a.primary {
85 | background: var(--foreground);
86 | color: var(--background);
87 | gap: 8px;
88 | }
89 |
90 | a.secondary {
91 | border-color: var(--gray-alpha-200);
92 | min-width: 158px;
93 | }
94 |
95 | .footer {
96 | grid-row-start: 3;
97 | display: flex;
98 | gap: 24px;
99 | }
100 |
101 | .footer a {
102 | display: flex;
103 | align-items: center;
104 | gap: 8px;
105 | }
106 |
107 | .footer img {
108 | flex-shrink: 0;
109 | }
110 |
111 | /* Enable hover only on non-touch devices */
112 | @media (hover: hover) and (pointer: fine) {
113 | a.primary:hover {
114 | background: var(--button-primary-hover);
115 | border-color: transparent;
116 | }
117 |
118 | a.secondary:hover {
119 | background: var(--button-secondary-hover);
120 | border-color: transparent;
121 | }
122 |
123 | .footer a:hover {
124 | text-decoration: underline;
125 | text-underline-offset: 4px;
126 | }
127 | }
128 |
129 | @media (max-width: 600px) {
130 | .page {
131 | padding: 32px;
132 | padding-bottom: 80px;
133 | }
134 |
135 | .main {
136 | align-items: center;
137 | }
138 |
139 | .main ol {
140 | text-align: center;
141 | }
142 |
143 | .ctas {
144 | flex-direction: column;
145 | }
146 |
147 | .ctas a {
148 | font-size: 14px;
149 | height: 40px;
150 | padding: 0 16px;
151 | }
152 |
153 | a.secondary {
154 | min-width: auto;
155 | }
156 |
157 | .footer {
158 | flex-wrap: wrap;
159 | align-items: center;
160 | justify-content: center;
161 | }
162 | }
163 |
164 | @media (prefers-color-scheme: dark) {
165 | .logo {
166 | filter: invert();
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/packages/logfire-node/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # logfire
2 |
3 | ## 0.11.2
4 |
5 | ### Patch Changes
6 |
7 | - 79032ef: Fix Scrubbing configuration. Scrubbing now works even when scope is not set
8 |
9 | ## 0.11.1
10 |
11 | ### Patch Changes
12 |
13 | - 26db714: Fix publish
14 |
15 | ## 0.11.0
16 |
17 | ### Minor Changes
18 |
19 | - 28eb056: BREAKING CHANGE: Package renamed from `logfire` to `@pydantic/logfire-node`.
20 |
21 | This change clarifies that this package is the Node.js-specific SDK with OpenTelemetry auto-instrumentation.
22 |
23 | **Migration Guide**:
24 |
25 | - Update package.json: Change `"logfire"` to `"@pydantic/logfire-node"`
26 | - Update imports: Change `from 'logfire'` to `from '@pydantic/logfire-node'`
27 | - Run `npm install` to update lockfiles
28 |
29 | The package functionality remains identical. This is purely a naming change.
30 |
31 | **Why this change?**
32 | The core API package (now simply called `logfire`) is used across all runtimes. The Node.js SDK with auto-instrumentation is a more specialized package and should have a scoped, descriptive name.
33 |
34 | ### Patch Changes
35 |
36 | - Updated dependencies [28eb056]
37 | - logfire@0.11.0
38 |
39 | ## 0.10.0
40 |
41 | ### Minor Changes
42 |
43 | - 03df4fb: Add default export to packages. Using the default import is equivalent to the star import.
44 |
45 | ### Patch Changes
46 |
47 | - Updated dependencies [03df4fb]
48 | - @pydantic/logfire-api@0.9.0
49 |
50 | ## 0.9.1
51 |
52 | ### Patch Changes
53 |
54 | - 258969c: Update READMEs
55 |
56 | ## 0.9.0
57 |
58 | ### Minor Changes
59 |
60 | - 413ff56: Support logging spans in the console
61 |
62 | ## 0.8.0
63 |
64 | ### Minor Changes
65 |
66 | - 71f46db: Auto-close spans opened with logfire.span
67 |
68 | ### Patch Changes
69 |
70 | - Updated dependencies [71f46db]
71 | - @pydantic/logfire-api@0.6.0
72 |
73 | ## 0.7.0
74 |
75 | ### Minor Changes
76 |
77 | - 2a62de6: Support passing additional instrumentations
78 |
79 | ## 0.6.0
80 |
81 | ### Minor Changes
82 |
83 | - 478e045: Experimental browser support
84 |
85 | ### Patch Changes
86 |
87 | - Updated dependencies [478e045]
88 | - @pydantic/logfire-api@0.5.0
89 |
90 | ## 0.5.2
91 |
92 | ### Patch Changes
93 |
94 | - cd2ac40: Fix attribute serialization
95 | - Updated dependencies [cd2ac40]
96 | - @pydantic/logfire-api@0.4.1
97 |
98 | ## 0.5.1
99 |
100 | ### Patch Changes
101 |
102 | - 14833ef: Fix typo in interface name
103 |
104 | ## 0.5.0
105 |
106 | ### Minor Changes
107 |
108 | - e1dc8d0: Allow configuration of node auto instrumentations
109 |
110 | ## 0.4.1
111 |
112 | ### Patch Changes
113 |
114 | - 8dbb603: Fix for not picking up environment
115 |
116 | ## 0.4.0
117 |
118 | ### Minor Changes
119 |
120 | - dc0a537: Support for EU tokens. Support span message formatting.
121 | - 65274e3: Support us/eu tokens
122 |
123 | ### Patch Changes
124 |
125 | - Updated dependencies [dc0a537]
126 | - @pydantic/logfire-api@0.4.0
127 |
128 | ## 0.3.0
129 |
130 | ### Minor Changes
131 |
132 | - 6fa1410: API updates, fixes for span kind
133 |
134 | ### Patch Changes
135 |
136 | - Updated dependencies [6fa1410]
137 | - @pydantic/logfire-api@0.3.0
138 |
139 | ## 0.2.2
140 |
141 | ### Patch Changes
142 |
143 | - a391811: Fix for a peer package
144 |
145 | ## 0.2.1
146 |
147 | ### Patch Changes
148 |
149 | - 838ba5d: Fix packages publish settings.
150 | - Updated dependencies [838ba5d]
151 | - @pydantic/logfire-api@0.2.1
152 |
153 | ## 0.2.0
154 |
155 | ### Minor Changes
156 |
157 | - 0f0ce8f: Initial release.
158 |
159 | ### Patch Changes
160 |
161 | - Updated dependencies [0f0ce8f]
162 | - @pydantic/logfire-api@0.2.0
163 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/logfireApiConfig.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable perfectionist/sort-objects */
2 | import { Context, context as ContextAPI, trace as TraceAPI, Tracer } from '@opentelemetry/api'
3 |
4 | import { BaseScrubber, LogfireAttributeScrubber, NoopAttributeScrubber, ScrubCallback } from './AttributeScrubber'
5 | import { DEFAULT_OTEL_SCOPE } from './constants'
6 |
7 | export * from './AttributeScrubber'
8 | export { serializeAttributes } from './serializeAttributes'
9 |
10 | export interface ScrubbingOptions {
11 | callback?: ScrubCallback
12 | extraPatterns?: string[]
13 | }
14 |
15 | export interface LogfireApiConfigOptions {
16 | otelScope?: string
17 | /**
18 | * Options for scrubbing sensitive data. Set to False to disable.
19 | */
20 | scrubbing?: false | ScrubbingOptions
21 | }
22 |
23 | export type SendToLogfire = 'if-token-present' | boolean | undefined
24 |
25 | export const Level = {
26 | Trace: 1 as const,
27 | Debug: 5 as const,
28 | Info: 9 as const,
29 | Notice: 10 as const,
30 | Warning: 13 as const,
31 | Error: 17 as const,
32 | Fatal: 21 as const,
33 | }
34 |
35 | export type Env = Record
36 |
37 | export type LogFireLevel = (typeof Level)[keyof typeof Level]
38 |
39 | export interface LogOptions {
40 | level?: LogFireLevel
41 | log?: true
42 | tags?: string[]
43 | }
44 |
45 | export interface LogfireApiConfig {
46 | context: Context
47 | otelScope: string
48 | scrubber: BaseScrubber
49 | tracer: Tracer
50 | }
51 |
52 | export interface RegionData {
53 | baseUrl: string
54 | gcpRegion: string
55 | }
56 |
57 | const DEFAULT_LOGFIRE_API_CONFIG: LogfireApiConfig = {
58 | get context() {
59 | return ContextAPI.active()
60 | },
61 | otelScope: DEFAULT_OTEL_SCOPE,
62 | scrubber: new LogfireAttributeScrubber(),
63 | tracer: TraceAPI.getTracer(DEFAULT_OTEL_SCOPE),
64 | }
65 |
66 | export const logfireApiConfig: LogfireApiConfig = DEFAULT_LOGFIRE_API_CONFIG
67 |
68 | export function configureLogfireApi(config: LogfireApiConfigOptions) {
69 | if (config.scrubbing !== undefined) {
70 | logfireApiConfig.scrubber = resolveScrubber(config.scrubbing)
71 | }
72 |
73 | if (config.otelScope !== undefined) {
74 | logfireApiConfig.otelScope = config.otelScope
75 | logfireApiConfig.tracer = TraceAPI.getTracer(config.otelScope)
76 | }
77 | }
78 |
79 | function resolveScrubber(scrubbing: LogfireApiConfigOptions['scrubbing']) {
80 | if (scrubbing !== undefined) {
81 | if (scrubbing === false) {
82 | return new NoopAttributeScrubber()
83 | } else {
84 | return new LogfireAttributeScrubber(scrubbing.extraPatterns, scrubbing.callback)
85 | }
86 | } else {
87 | return new LogfireAttributeScrubber()
88 | }
89 | }
90 |
91 | export function resolveSendToLogfire(env: Env, option: SendToLogfire, token: string | undefined) {
92 | const sendToLogfireConfig = option ?? env.LOGFIRE_SEND_TO_LOGFIRE ?? 'if-token-present'
93 |
94 | if (sendToLogfireConfig === 'if-token-present') {
95 | if (token) {
96 | return true
97 | } else {
98 | return false
99 | }
100 | } else {
101 | return Boolean(sendToLogfireConfig)
102 | }
103 | }
104 |
105 | export function resolveBaseUrl(env: Env, passedUrl: string | undefined, token: string) {
106 | let url = passedUrl ?? env.LOGFIRE_BASE_URL ?? getBaseUrlFromToken(token)
107 | if (url.endsWith('/')) {
108 | url = url.slice(0, -1)
109 | }
110 | return url
111 | }
112 |
113 | const PYDANTIC_LOGFIRE_TOKEN_PATTERN = /^(?pylf_v(?[0-9]+)_(?[a-z]+)_)(?[a-zA-Z0-9]+)$/
114 |
115 | const REGIONS: Record = {
116 | eu: {
117 | baseUrl: 'https://logfire-eu.pydantic.dev',
118 | gcpRegion: 'europe-west4',
119 | },
120 | us: {
121 | baseUrl: 'https://logfire-us.pydantic.dev',
122 | gcpRegion: 'us-east4',
123 | },
124 | }
125 |
126 | function getBaseUrlFromToken(token: string | undefined): string {
127 | let regionKey = 'us'
128 | if (token) {
129 | const match = PYDANTIC_LOGFIRE_TOKEN_PATTERN.exec(token)
130 | if (match) {
131 | const region = match.groups?.region
132 | if (region && region in REGIONS) {
133 | regionKey = region
134 | }
135 | }
136 | }
137 | const regionData = REGIONS[regionKey]
138 | if (!regionData) {
139 | throw new Error(`Unknown region in token: ${regionKey}. Valid regions are: ${Object.keys(REGIONS).join(', ')}`)
140 | }
141 | return regionData.baseUrl
142 | }
143 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/OtlpTransformerTypes.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 | /*
3 | * Copyright The OpenTelemetry Authors
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * https://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | /** Properties of an ArrayValue. */
19 | export interface IArrayValue {
20 | /** ArrayValue values */
21 | values: IAnyValue[]
22 | }
23 |
24 | /** Properties of a KeyValueList. */
25 | export interface IKeyValueList {
26 | /** KeyValueList values */
27 | values: IKeyValue[]
28 | }
29 |
30 | export interface LongBits {
31 | high: number
32 | low: number
33 | }
34 |
35 | export type Fixed64 = LongBits | number | string
36 |
37 | /** Properties of an AnyValue. */
38 | export interface IAnyValue {
39 | /** AnyValue arrayValue */
40 | arrayValue?: IArrayValue
41 |
42 | /** AnyValue boolValue */
43 | boolValue?: boolean | null
44 |
45 | /** AnyValue bytesValue */
46 | bytesValue?: Uint8Array
47 |
48 | /** AnyValue doubleValue */
49 | doubleValue?: null | number
50 |
51 | /** AnyValue intValue */
52 | intValue?: null | number
53 |
54 | /** AnyValue kvlistValue */
55 | kvlistValue?: IKeyValueList
56 |
57 | /** AnyValue stringValue */
58 | stringValue?: null | string
59 | }
60 |
61 | /** Properties of a KeyValue. */
62 | export interface IKeyValue {
63 | /** KeyValue key */
64 | key: string
65 |
66 | /** KeyValue value */
67 | value: IAnyValue
68 | }
69 |
70 | /** Properties of a Resource. */
71 | export interface Resource {
72 | /** Resource attributes */
73 | attributes: IKeyValue[]
74 |
75 | /** Resource droppedAttributesCount */
76 | droppedAttributesCount: number
77 | }
78 |
79 | /** Properties of an ExportTraceServiceRequest. */
80 | export interface IExportTraceServiceRequest {
81 | /** ExportTraceServiceRequest resourceSpans */
82 | resourceSpans?: IResourceSpans[]
83 | }
84 |
85 | /** Properties of a ResourceSpans. */
86 | export interface IResourceSpans {
87 | /** ResourceSpans resource */
88 | resource?: Resource
89 |
90 | /** ResourceSpans schemaUrl */
91 | schemaUrl?: string
92 |
93 | /** ResourceSpans scopeSpans */
94 | scopeSpans: IScopeSpans[]
95 | }
96 |
97 | /** Properties of an ScopeSpans. */
98 | export interface IScopeSpans {
99 | /** IScopeSpans schemaUrl */
100 | schemaUrl?: null | string
101 |
102 | /** IScopeSpans scope */
103 | scope?: IInstrumentationScope
104 |
105 | /** IScopeSpans spans */
106 | spans?: ISpan[]
107 | }
108 |
109 | /** Properties of an InstrumentationScope. */
110 | export interface IInstrumentationScope {
111 | /** InstrumentationScope attributes */
112 | attributes?: IKeyValue[]
113 |
114 | /** InstrumentationScope droppedAttributesCount */
115 | droppedAttributesCount?: number
116 |
117 | /** InstrumentationScope name */
118 | name: string
119 |
120 | /** InstrumentationScope version */
121 | version?: string
122 | }
123 | /** Properties of a Span. */
124 | export interface ISpan {
125 | /** Span attributes */
126 | attributes: IKeyValue[]
127 |
128 | /** Span droppedAttributesCount */
129 | droppedAttributesCount: number
130 |
131 | /** Span droppedEventsCount */
132 | droppedEventsCount: number
133 |
134 | /** Span droppedLinksCount */
135 | droppedLinksCount: number
136 |
137 | /** Span endTimeUnixNano */
138 | endTimeUnixNano: Fixed64
139 |
140 | /** Span events */
141 | // events: IEvent[]
142 |
143 | /** Span kind */
144 | // kind: ESpanKind
145 |
146 | /** Span links */
147 | // links: ILink[]
148 |
149 | /** Span name */
150 | name: string
151 |
152 | /** Span parentSpanId */
153 | parentSpanId?: string | Uint8Array
154 |
155 | /** Span spanId */
156 | spanId: string | Uint8Array
157 |
158 | /** Span startTimeUnixNano */
159 | startTimeUnixNano: Fixed64
160 |
161 | /** Span status */
162 | // status: IStatus
163 |
164 | /** Span traceId */
165 | traceId: string | Uint8Array
166 |
167 | /** Span traceState */
168 | traceState?: null | string
169 | }
170 |
--------------------------------------------------------------------------------
/packages/logfire-browser/src/LogfireSpanProcessor.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2 | /* eslint-disable @typescript-eslint/no-deprecated */
3 | import { Context } from '@opentelemetry/api'
4 | import { ExportResult, ExportResultCode, hrTimeToMicroseconds } from '@opentelemetry/core'
5 | import { ReadableSpan, SimpleSpanProcessor, Span, SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-web'
6 | import { ATTR_HTTP_URL } from '@opentelemetry/semantic-conventions/incubating'
7 |
8 | // not present in the semantic conventions
9 | const ATTR_TARGET_XPATH = 'target_xpath'
10 | const ATTR_EVENT_TYPE = 'event_type'
11 |
12 | export const LevelLabels = {
13 | 1: 'trace',
14 | 5: 'debug',
15 | 9: 'info',
16 | 10: 'notice',
17 | 13: 'warning',
18 | 17: 'error',
19 | 21: 'fatal',
20 | } as const
21 |
22 | const Colors = {
23 | debug: '#E3E3E3',
24 | error: '#EA4335',
25 | fatal: '#EA4335',
26 | info: '#9EC1FB',
27 | notice: '#A5D490',
28 | 'on-debug': '#636262',
29 | 'on-error': '#FFEDE9',
30 | 'on-fatal': '#FFEDE9',
31 | 'on-info': '#063175',
32 | 'on-notice': '#222222',
33 | 'on-trace': '#636262',
34 | 'on-warning': '#613A0D',
35 | trace: '#E3E3E3',
36 | warning: '#EFB77A',
37 | } as const
38 |
39 | class LogfireConsoleSpanExporter implements SpanExporter {
40 | export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
41 | this.sendSpans(spans, resultCallback)
42 | }
43 |
44 | forceFlush(): Promise {
45 | return Promise.resolve()
46 | }
47 | shutdown(): Promise {
48 | this.sendSpans([])
49 | return this.forceFlush()
50 | }
51 | /**
52 | * converts span info into more readable format
53 | * @param span
54 | */
55 | private exportInfo(span: ReadableSpan) {
56 | return {
57 | attributes: span.attributes,
58 | duration: hrTimeToMicroseconds(span.duration),
59 | events: span.events,
60 | id: span.spanContext().spanId,
61 | instrumentationScope: span.instrumentationScope,
62 | kind: span.kind,
63 | links: span.links,
64 | name: span.name,
65 | parentSpanContext: span.parentSpanContext,
66 | resource: {
67 | attributes: span.resource.attributes,
68 | },
69 | status: span.status,
70 | timestamp: hrTimeToMicroseconds(span.startTime),
71 | traceId: span.spanContext().traceId,
72 | traceState: span.spanContext().traceState?.serialize(),
73 | }
74 | }
75 |
76 | private sendSpans(spans: ReadableSpan[], done?: (result: ExportResult) => void): void {
77 | for (const span of spans) {
78 | const type = LevelLabels[span.attributes['logfire.level_num'] as keyof typeof LevelLabels] ?? 'info'
79 |
80 | const { attributes, name, ...rest } = this.exportInfo(span)
81 | console.log(
82 | `%cLogfire %c${type}`,
83 | 'background-color: #E520E9; color: #FFFFFF',
84 | `background-color: ${Colors[`on-${type}`]}; color: ${Colors[type]}`,
85 | name,
86 | attributes,
87 | rest
88 | )
89 | }
90 | if (done) {
91 | done({ code: ExportResultCode.SUCCESS })
92 | }
93 | }
94 | }
95 |
96 | export class LogfireSpanProcessor implements SpanProcessor {
97 | private console?: SpanProcessor
98 | private wrapped: SpanProcessor
99 |
100 | constructor(wrapped: SpanProcessor, enableConsole: boolean) {
101 | if (enableConsole) {
102 | this.console = new SimpleSpanProcessor(new LogfireConsoleSpanExporter())
103 | }
104 | this.wrapped = wrapped
105 | }
106 |
107 | async forceFlush(): Promise {
108 | await this.console?.forceFlush()
109 | return this.wrapped.forceFlush()
110 | }
111 |
112 | onEnd(span: ReadableSpan): void {
113 | this.console?.onEnd(span)
114 | // Note: this is too late for the regular node instrumentation. The opentelemetry API rejects the non-primitive attribute values.
115 | // Instead, the serialization happens at the `logfire.span, logfire.startSpan`, etc.
116 | // Object.assign(span.attributes, serializeAttributes(span.attributes))
117 | this.wrapped.onEnd(span)
118 | }
119 |
120 | onStart(span: Span, parentContext: Context): void {
121 | // make the fetch spans more descriptive
122 | if (ATTR_HTTP_URL in span.attributes) {
123 | const url = new URL(span.attributes[ATTR_HTTP_URL] as string)
124 | Reflect.set(span, 'name', `${span.name} ${url.pathname}`)
125 | }
126 |
127 | // same for the interaction spans
128 | if (ATTR_TARGET_XPATH in span.attributes) {
129 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
130 | Reflect.set(span, 'name', `${span.attributes[ATTR_EVENT_TYPE] ?? 'unknown'} ${span.attributes[ATTR_TARGET_XPATH] ?? ''}`)
131 | }
132 | this.console?.onStart(span, parentContext)
133 | this.wrapped.onStart(span, parentContext)
134 | }
135 |
136 | async shutdown(): Promise {
137 | await this.console?.shutdown()
138 | return this.wrapped.shutdown()
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/sdk.ts:
--------------------------------------------------------------------------------
1 | import { diag, DiagConsoleLogger, metrics } from '@opentelemetry/api'
2 | import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
3 | import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
4 | import { W3CTraceContextPropagator } from '@opentelemetry/core'
5 | import { detectResources, envDetector, resourceFromAttributes } from '@opentelemetry/resources'
6 | import { MeterProvider } from '@opentelemetry/sdk-metrics'
7 | import { NodeSDK } from '@opentelemetry/sdk-node'
8 | import {
9 | ATTR_SERVICE_NAME,
10 | ATTR_SERVICE_VERSION,
11 | ATTR_TELEMETRY_SDK_LANGUAGE,
12 | ATTR_TELEMETRY_SDK_NAME,
13 | ATTR_TELEMETRY_SDK_VERSION,
14 | TELEMETRY_SDK_LANGUAGE_VALUE_NODEJS,
15 | } from '@opentelemetry/semantic-conventions'
16 | import {
17 | ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
18 | ATTR_VCS_REPOSITORY_REF_REVISION,
19 | ATTR_VCS_REPOSITORY_URL_FULL,
20 | } from '@opentelemetry/semantic-conventions/incubating'
21 | import { reportError, ULIDGenerator } from 'logfire'
22 |
23 | import { logfireConfig } from './logfireConfig'
24 | import { periodicMetricReader } from './metricExporter'
25 | import { logfireSpanProcessor } from './traceExporter'
26 | import { removeEmptyKeys } from './utils'
27 |
28 | const LOGFIRE_ATTRIBUTES_NAMESPACE = 'logfire'
29 | const RESOURCE_ATTRIBUTES_CODE_ROOT_PATH = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.code.root_path`
30 |
31 | export function start() {
32 | if (logfireConfig.diagLogLevel !== undefined) {
33 | diag.setLogger(new DiagConsoleLogger(), logfireConfig.diagLogLevel)
34 | }
35 |
36 | const resource = resourceFromAttributes(
37 | removeEmptyKeys({
38 | [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: logfireConfig.deploymentEnvironment,
39 | [ATTR_SERVICE_NAME]: logfireConfig.serviceName,
40 | [ATTR_SERVICE_VERSION]: logfireConfig.serviceVersion,
41 | [ATTR_TELEMETRY_SDK_LANGUAGE]: TELEMETRY_SDK_LANGUAGE_VALUE_NODEJS,
42 | [ATTR_TELEMETRY_SDK_NAME]: 'logfire',
43 | // eslint-disable-next-line no-undef
44 | [ATTR_TELEMETRY_SDK_VERSION]: PACKAGE_VERSION,
45 |
46 | [ATTR_VCS_REPOSITORY_REF_REVISION]: logfireConfig.codeSource?.revision,
47 | [ATTR_VCS_REPOSITORY_URL_FULL]: logfireConfig.codeSource?.repository,
48 | [RESOURCE_ATTRIBUTES_CODE_ROOT_PATH]: logfireConfig.codeSource?.rootPath,
49 | })
50 | ).merge(detectResources({ detectors: [envDetector] }))
51 |
52 | // use AsyncLocalStorageContextManager to manage parent <> child relationshps in async functions
53 | const contextManager = new AsyncLocalStorageContextManager()
54 |
55 | const propagator = logfireConfig.distributedTracing ? new W3CTraceContextPropagator() : undefined
56 |
57 | const processor = logfireSpanProcessor(logfireConfig.console)
58 | const sdk = new NodeSDK({
59 | autoDetectResources: false,
60 | contextManager,
61 | idGenerator: new ULIDGenerator(),
62 | instrumentations: [getNodeAutoInstrumentations(logfireConfig.nodeAutoInstrumentations), ...logfireConfig.instrumentations],
63 | metricReader: logfireConfig.metrics === false ? undefined : periodicMetricReader(),
64 | resource,
65 | spanProcessors: [processor, ...logfireConfig.additionalSpanProcessors],
66 | textMapPropagator: propagator,
67 | })
68 |
69 | if (logfireConfig.metrics && 'additionalReaders' in logfireConfig.metrics) {
70 | const meterProvider = new MeterProvider({ readers: [periodicMetricReader(), ...logfireConfig.metrics.additionalReaders], resource })
71 | metrics.setGlobalMeterProvider(meterProvider)
72 | }
73 |
74 | sdk.start()
75 | diag.info('logfire: starting')
76 |
77 | process.on('uncaughtExceptionMonitor', (error: Error) => {
78 | diag.info('logfire: caught uncaught exception', error.message)
79 | reportError(error.message, error, {})
80 |
81 | // eslint-disable-next-line no-void
82 | void processor.forceFlush()
83 | })
84 |
85 | process.on('unhandledRejection', (reason: Error) => {
86 | diag.error('unhandled rejection', reason)
87 |
88 | if (reason instanceof Error) {
89 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
90 | reportError(reason.message ?? 'error', reason, {})
91 | }
92 | // eslint-disable-next-line no-void
93 | void processor.forceFlush()
94 | })
95 |
96 | // gracefully shut down the SDK on process exit
97 | process.on('SIGTERM', () => {
98 | sdk
99 | .shutdown()
100 | .catch((e: unknown) => {
101 | diag.warn('logfire SDK: error shutting down', e)
102 | })
103 | .finally(() => {
104 | diag.info('logfire SDK: shutting down')
105 | })
106 | })
107 |
108 | let _shutdown = false
109 |
110 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
111 | process.on('beforeExit', async () => {
112 | if (!_shutdown) {
113 | try {
114 | await sdk.shutdown()
115 | } catch (e) {
116 | diag.warn('logfire SDK: error shutting down', e)
117 | } finally {
118 | _shutdown = true
119 | diag.info('logfire SDK: shutting down')
120 | }
121 | }
122 | })
123 | }
124 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/index.ts:
--------------------------------------------------------------------------------
1 | import { type ReadableSpan, SimpleSpanProcessor, SpanProcessor } from '@opentelemetry/sdk-trace-base'
2 | import { instrument as baseInstrument, TraceConfig } from '@pydantic/otel-cf-workers'
3 | import {
4 | configureLogfireApi,
5 | debug,
6 | error,
7 | fatal,
8 | info,
9 | Level,
10 | log,
11 | logfireApiConfig,
12 | LogfireAttributeScrubber,
13 | NoopAttributeScrubber,
14 | notice,
15 | reportError,
16 | resolveBaseUrl,
17 | resolveSendToLogfire,
18 | type ScrubbingOptions,
19 | serializeAttributes,
20 | span,
21 | startSpan,
22 | trace,
23 | ULIDGenerator,
24 | warning,
25 | } from 'logfire'
26 |
27 | // Import all exports to construct default export
28 | import * as exportTailEventsExports from './exportTailEventsToLogfire'
29 | import { LogfireCloudflareConsoleSpanExporter } from './LogfireCloudflareConsoleSpanExporter'
30 | import { TailWorkerExporter } from './TailWorkerExporter'
31 | export * from './exportTailEventsToLogfire'
32 |
33 | type Env = Record
34 |
35 | type ConfigOptionsBase = Pick<
36 | TraceConfig,
37 | 'environment' | 'fetch' | 'handlers' | 'instrumentation' | 'propagator' | 'sampling' | 'scope' | 'service'
38 | >
39 |
40 | export interface InProcessConfigOptions extends ConfigOptionsBase {
41 | /**
42 | * Additional span processors to add to the tracer provider.
43 | */
44 | additionalSpanProcessors?: SpanProcessor[]
45 | baseUrl?: string
46 | /**
47 | * Whether to log the spans to the console in addition to sending them to the Logfire API.
48 | */
49 | console?: boolean
50 | /**
51 | * Options for scrubbing sensitive data. Set to False to disable.
52 | */
53 | scrubbing?: false | ScrubbingOptions
54 | }
55 |
56 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
57 | export interface TailConfigOptions extends ConfigOptionsBase {}
58 |
59 | function getInProcessConfig(config: InProcessConfigOptions): (env: Env) => TraceConfig {
60 | return (env: Env): TraceConfig => {
61 | const { LOGFIRE_ENVIRONMENT: envDeploymentEnvironment, LOGFIRE_TOKEN: token = '' } = env
62 |
63 | const baseUrl = resolveBaseUrl(env, config.baseUrl, token)
64 | const resolvedEnvironment = config.environment ?? envDeploymentEnvironment
65 |
66 | const additionalSpanProcessors = config.additionalSpanProcessors ?? []
67 |
68 | if (config.console) {
69 | additionalSpanProcessors.push(new SimpleSpanProcessor(new LogfireCloudflareConsoleSpanExporter()))
70 | }
71 |
72 | return Object.assign({}, config, {
73 | additionalSpanProcessors,
74 | environment: resolvedEnvironment,
75 | exporter: {
76 | headers: { Authorization: token },
77 | url: `${baseUrl}/v1/traces`,
78 | },
79 | idGenerator: new ULIDGenerator(),
80 | postProcessor: (spans: ReadableSpan[]) => postProcessAttributes(spans),
81 | }) satisfies TraceConfig
82 | }
83 | }
84 |
85 | export function getTailConfig(config: TailConfigOptions): (env: Env) => TraceConfig {
86 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
87 | return (_env: Env): TraceConfig => {
88 | return Object.assign({}, config, {
89 | exporter: new TailWorkerExporter(),
90 | idGenerator: new ULIDGenerator(),
91 | })
92 | }
93 | }
94 |
95 | export function instrumentInProcess(handler: T, config: InProcessConfigOptions): T {
96 | if (config.scrubbing !== undefined) {
97 | configureLogfireApi({ scrubbing: config.scrubbing })
98 | }
99 | return baseInstrument(handler, getInProcessConfig(config)) as T
100 | }
101 |
102 | export function instrumentTail(handler: T, config: TailConfigOptions): T {
103 | return baseInstrument(handler, getTailConfig(config)) as T
104 | }
105 |
106 | /**
107 | * Alias for `instrumentInProcess` to maintain compatibility with previous versions.
108 | */
109 | export const instrument = instrumentInProcess
110 |
111 | function postProcessAttributes(spans: ReadableSpan[]) {
112 | for (const span of spans) {
113 | for (const attrKey of Object.keys(span.attributes)) {
114 | const attrVal = span.attributes[attrKey]
115 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
116 | if (attrVal === undefined || attrVal === null) {
117 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
118 | delete span.attributes[attrKey]
119 | }
120 | }
121 | Object.assign(span.attributes, serializeAttributes(span.attributes))
122 | }
123 | return spans
124 | }
125 |
126 | // Create default export by listing all exports explicitly
127 | export default {
128 | ...exportTailEventsExports,
129 | configureLogfireApi,
130 | debug,
131 | error,
132 | fatal,
133 | getTailConfig,
134 | info,
135 | instrument,
136 | instrumentInProcess,
137 | instrumentTail,
138 | // Re-export all from logfire
139 | Level,
140 | log,
141 | logfireApiConfig,
142 | LogfireAttributeScrubber,
143 | NoopAttributeScrubber,
144 | notice,
145 | reportError,
146 | resolveBaseUrl,
147 | resolveSendToLogfire,
148 | serializeAttributes,
149 | span,
150 | startSpan,
151 | trace,
152 | ULIDGenerator,
153 | warning,
154 | }
155 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Overview
6 |
7 | This is a monorepo for the **Pydantic Logfire JavaScript SDK** - an observability platform built on OpenTelemetry. The repository contains multiple packages for different JavaScript runtimes (Node.js, browsers, Cloudflare Workers, etc.) and usage examples.
8 |
9 | ## Repository Structure
10 |
11 | This is an **npm workspace monorepo** managed with **Turborepo**:
12 |
13 | - `packages/logfire-node` - Node.js SDK with automatic OpenTelemetry instrumentation
14 | - `packages/logfire-api` - Core API package (published as `logfire`) that can be used standalone for manual tracing (no auto-instrumentation)
15 | - `packages/logfire-cf-workers` - Cloudflare Workers integration
16 | - `packages/logfire-browser` - Browser/web SDK
17 | - `packages/tooling-config` - Shared build and linting configuration
18 | - `examples/` - Working examples for various platforms (Express, Next.js, Deno, Cloudflare Workers, etc.)
19 |
20 | ## Core Architecture
21 |
22 | ### Package Relationships
23 |
24 | - `logfire` (published from `packages/logfire-api`) is the base package that provides the core tracing API (`span`, `info`, `debug`, `error`, etc.) - it wraps OpenTelemetry's trace API with convenience methods
25 | - `@pydantic/logfire-node` (from `packages/logfire-node`) depends on `logfire` and adds automatic instrumentation via `@opentelemetry/auto-instrumentations-node`
26 | - `@pydantic/logfire-cf-workers` depends on `logfire` and adds Cloudflare Workers-specific instrumentation
27 | - `@pydantic/logfire-browser` depends on `logfire` and adds browser-specific instrumentation
28 |
29 | ### Key Concepts
30 |
31 | **Trace API** (`logfire` package):
32 |
33 | - Provides convenience wrappers around OpenTelemetry spans with log levels (trace, debug, info, notice, warn, error, fatal)
34 | - Uses message template formatting with attribute extraction (see `formatter.ts`)
35 | - Uses ULID for trace ID generation (see `ULIDGenerator.ts`)
36 | - Supports attribute scrubbing for sensitive data (see `AttributeScrubber.ts`)
37 |
38 | **Configuration** (`@pydantic/logfire-node` package):
39 |
40 | - `configure()` function in `logfireConfig.ts` handles SDK initialization
41 | - Configuration can be provided programmatically or via environment variables:
42 | - `LOGFIRE_TOKEN` - Authentication token
43 | - `LOGFIRE_SERVICE_NAME` - Service name
44 | - `LOGFIRE_SERVICE_VERSION` - Service version
45 | - `LOGFIRE_ENVIRONMENT` - Deployment environment
46 | - `LOGFIRE_CONSOLE` - Enable console output
47 | - `LOGFIRE_SEND_TO_LOGFIRE` - Toggle sending to Logfire backend
48 | - `LOGFIRE_DISTRIBUTED_TRACING` - Enable/disable trace context propagation
49 |
50 | **Span Creation**:
51 |
52 | - `startSpan()` - Creates a span without setting it on context (manual mode)
53 | - `span()` - Creates a span, executes a callback, and auto-ends the span (recommended)
54 | - `info()`, `debug()`, `error()`, etc. - Convenience methods that create log-type spans
55 | - All spans use message templates with attribute extraction (e.g., `"User {user_id} logged in"`)
56 |
57 | ## Common Commands
58 |
59 | ### Development Setup
60 |
61 | ```bash
62 | npm install
63 | ```
64 |
65 | ### Building
66 |
67 | ```bash
68 | # Build all packages
69 | npm run build
70 |
71 | # Build in watch mode (for development)
72 | npm run dev
73 | ```
74 |
75 | ### Testing
76 |
77 | ```bash
78 | # Run all tests
79 | npm run test
80 |
81 | # Run tests for a specific package
82 | cd packages/logfire-node && npm test
83 | ```
84 |
85 | ### Linting and Type Checking
86 |
87 | ```bash
88 | # Run both typecheck and lint across all packages
89 | npm run ci
90 |
91 | # Just linting
92 | turbo lint
93 |
94 | # Just type checking
95 | turbo typecheck
96 | ```
97 |
98 | ### Working with Examples
99 |
100 | Start an example to test changes:
101 |
102 | ```bash
103 | # Navigate to an example
104 | cd examples/node # or express, nextjs, cf-worker, etc.
105 |
106 | # Install dependencies (if needed)
107 | npm install
108 |
109 | # Run the example (check the example's package.json for scripts)
110 | npm start # or npm run dev
111 | ```
112 |
113 | ### Changesets (Version Management)
114 |
115 | This project uses Changesets for version management:
116 |
117 | ```bash
118 | # Add a changeset when making changes
119 | npm run changeset-add
120 |
121 | # Publish packages (maintainers only)
122 | npm run release
123 | ```
124 |
125 | ### Running a Single Test
126 |
127 | ```bash
128 | # Navigate to the package
129 | cd packages/logfire-api # or packages/logfire-node
130 |
131 | # Run vitest with a filter
132 | npm test -- -t "test name pattern"
133 | ```
134 |
135 | ## Development Workflow
136 |
137 | 1. Make changes in `packages/` source code
138 | 2. Run `npm run build` to rebuild packages (or `npm run dev` for watch mode)
139 | 3. Test changes using examples in `examples/` directory
140 | 4. Run `npm run ci` to ensure linting and type checking pass
141 | 5. Add a changeset if the changes warrant a version bump: `npm run changeset-add`
142 |
143 | ## Important Implementation Details
144 |
145 | ### Message Template Formatting
146 |
147 | The `logfireFormatWithExtras()` function in `formatter.ts` extracts attributes from message templates. For example:
148 |
149 | - `"User {user_id} logged in"` with `{ user_id: 123 }` becomes formatted message `"User 123 logged in"`
150 | - Extracted attributes are stored with special keys and used by the Logfire backend
151 |
152 | ### Attribute Scrubbing
153 |
154 | Sensitive data scrubbing is handled in `AttributeScrubber.ts`. By default, it redacts common sensitive patterns (passwords, tokens, API keys, etc.) using regex patterns.
155 |
156 | ### Span Types
157 |
158 | Spans have a `logfire.span_type` attribute:
159 |
160 | - `"log"` - Point-in-time events (no child spans expected)
161 | - `"span"` - Duration-based traces (can have child spans)
162 |
163 | ### ID Generation
164 |
165 | The SDK uses ULID (Universally Unique Lexicographically Sortable Identifier) for trace IDs by default, which provides time-ordered IDs for better performance.
166 |
167 | ### Build System
168 |
169 | - Uses Vite for building packages (see individual `vite.config.ts` files)
170 | - Shared Vite config is in `packages/tooling-config/vite-config.ts`
171 | - Outputs both ESM (`.js`) and CommonJS (`.cjs`) formats with corresponding TypeScript definitions
172 |
173 | ## Testing Notes
174 |
175 | - Tests use Vitest
176 | - Some packages have minimal tests (`--passWithNoTests` flag in package.json)
177 | - Test files are located alongside source files with `.test.ts` extension
178 |
179 | ## Node Version
180 |
181 | The project requires **Node.js 22** (see `engines` in root package.json).
182 |
183 | ## Package Manager
184 |
185 | Uses **npm 10.9.2** (enforced via `packageManager` field).
186 |
--------------------------------------------------------------------------------
/packages/logfire-browser/src/index.ts:
--------------------------------------------------------------------------------
1 | import { ContextManager, diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api'
2 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
3 | import { Instrumentation, registerInstrumentations } from '@opentelemetry/instrumentation'
4 | import { resourceFromAttributes } from '@opentelemetry/resources'
5 | import { BatchSpanProcessor, BufferConfig, StackContextManager, WebTracerProvider } from '@opentelemetry/sdk-trace-web'
6 | import {
7 | ATTR_SERVICE_NAME,
8 | ATTR_SERVICE_VERSION,
9 | ATTR_TELEMETRY_SDK_LANGUAGE,
10 | ATTR_TELEMETRY_SDK_NAME,
11 | ATTR_TELEMETRY_SDK_VERSION,
12 | ATTR_USER_AGENT_ORIGINAL,
13 | TELEMETRY_SDK_LANGUAGE_VALUE_WEBJS,
14 | } from '@opentelemetry/semantic-conventions'
15 | import {
16 | ATTR_BROWSER_BRANDS,
17 | ATTR_BROWSER_LANGUAGE,
18 | ATTR_BROWSER_MOBILE,
19 | ATTR_BROWSER_PLATFORM,
20 | ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
21 | } from '@opentelemetry/semantic-conventions/incubating'
22 | import {
23 | configureLogfireApi,
24 | debug,
25 | error,
26 | fatal,
27 | info,
28 | Level,
29 | log,
30 | logfireApiConfig,
31 | LogfireAttributeScrubber,
32 | NoopAttributeScrubber,
33 | notice,
34 | reportError,
35 | resolveBaseUrl,
36 | resolveSendToLogfire,
37 | type ScrubbingOptions,
38 | serializeAttributes,
39 | span,
40 | startSpan,
41 | trace,
42 | ULIDGenerator,
43 | warning,
44 | } from 'logfire'
45 |
46 | import { LogfireSpanProcessor } from './LogfireSpanProcessor'
47 | import { OTLPTraceExporterWithDynamicHeaders } from './OTLPTraceExporterWithDynamicHeaders'
48 | export { DiagLogLevel } from '@opentelemetry/api'
49 | export * from 'logfire'
50 |
51 | type TraceExporterConfig = NonNullable unknown ? T : never>
52 |
53 | export interface LogfireConfigOptions {
54 | /**
55 | * The configuration of the batch span processor.
56 | */
57 | batchSpanProcessorConfig?: BufferConfig
58 | /**
59 | * Whether to log the spans to the console in addition to sending them to the Logfire API.
60 | */
61 | console?: boolean
62 | /**
63 | * Pass a context manager (e.g. ZoneContextManager) to use.
64 | */
65 | contextManager?: ContextManager
66 |
67 | /**
68 | * Defines the available internal logging levels for the diagnostic logger.
69 | */
70 | diagLogLevel?: DiagLogLevel
71 |
72 | /**
73 | * The environment this service is running in, e.g. `staging` or `prod`. Sets the deployment.environment.name resource attribute. Useful for filtering within projects in the Logfire UI.
74 | * Defaults to the `LOGFIRE_ENVIRONMENT` environment variable.
75 | */
76 | environment?: string
77 | /**
78 | * The instrumentations to register - a common one [is the fetch instrumentation](https://www.npmjs.com/package/@opentelemetry/instrumentation-fetch).
79 | */
80 | instrumentations?: (Instrumentation | Instrumentation[])[]
81 | /**
82 | * Options for scrubbing sensitive data. Set to False to disable.
83 | */
84 | scrubbing?: false | ScrubbingOptions
85 |
86 | /**
87 | * Name of this service.
88 | */
89 | serviceName?: string
90 |
91 | /**
92 | * Version of this service.
93 | */
94 | serviceVersion?: string
95 |
96 | /**
97 | * configures the trace exporter.
98 | */
99 | traceExporterConfig?: TraceExporterConfig
100 |
101 | /**
102 | * Any additional HTTP headers to be sent with the trace exporter requests.
103 | * This is useful for authentication or other custom headers.
104 | */
105 | traceExporterHeaders?: () => Record
106 |
107 | /**
108 | * The URL of your trace exporter proxy endpoint.
109 | */
110 | traceUrl: string
111 | }
112 |
113 | function defaultTraceExporterHeaders() {
114 | return {}
115 | }
116 |
117 | export function configure(options: LogfireConfigOptions) {
118 | if (options.diagLogLevel !== undefined) {
119 | diag.setLogger(new DiagConsoleLogger(), options.diagLogLevel)
120 | }
121 |
122 | if (options.scrubbing !== undefined) {
123 | configureLogfireApi({ scrubbing: options.scrubbing })
124 | }
125 |
126 | const resource = resourceFromAttributes({
127 | [ATTR_BROWSER_LANGUAGE]: navigator.language,
128 | [ATTR_SERVICE_NAME]: options.serviceName ?? 'logfire-browser',
129 | [ATTR_SERVICE_VERSION]: options.serviceVersion ?? '0.0.1',
130 | [ATTR_TELEMETRY_SDK_LANGUAGE]: TELEMETRY_SDK_LANGUAGE_VALUE_WEBJS,
131 | [ATTR_TELEMETRY_SDK_NAME]: 'logfire-browser',
132 | ...(options.environment ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: options.environment } : {}),
133 | // eslint-disable-next-line no-undef
134 | [ATTR_TELEMETRY_SDK_VERSION]: PACKAGE_VERSION,
135 | ...(navigator.userAgentData
136 | ? {
137 | [ATTR_BROWSER_BRANDS]: navigator.userAgentData.brands.map((brand) => `${brand.brand} ${brand.version}`),
138 | [ATTR_BROWSER_MOBILE]: navigator.userAgentData.mobile,
139 | [ATTR_BROWSER_PLATFORM]: navigator.userAgentData.platform,
140 | }
141 | : {
142 | [ATTR_USER_AGENT_ORIGINAL]: navigator.userAgent,
143 | }),
144 | })
145 |
146 | diag.info('logfire-browser: starting')
147 | const tracerProvider = new WebTracerProvider({
148 | idGenerator: new ULIDGenerator(),
149 | resource,
150 | spanProcessors: [
151 | new LogfireSpanProcessor(
152 | new BatchSpanProcessor(
153 | new OTLPTraceExporterWithDynamicHeaders(
154 | { ...options.traceExporterConfig, url: options.traceUrl },
155 | options.traceExporterHeaders ?? defaultTraceExporterHeaders
156 | ),
157 | options.batchSpanProcessorConfig
158 | ),
159 | Boolean(options.console)
160 | ),
161 | ],
162 | })
163 |
164 | tracerProvider.register({
165 | contextManager: options.contextManager ?? new StackContextManager(),
166 | })
167 |
168 | const unregister = registerInstrumentations({
169 | instrumentations: options.instrumentations ?? [],
170 | tracerProvider,
171 | })
172 |
173 | return async () => {
174 | diag.info('logfire-browser: shutting down')
175 | unregister()
176 | await tracerProvider.forceFlush()
177 | await tracerProvider.shutdown()
178 | diag.info('logfire-browser: shut down complete')
179 | }
180 | }
181 |
182 | // Create default export by listing all exports explicitly
183 | export default {
184 | configure,
185 | configureLogfireApi,
186 | debug,
187 | DiagLogLevel,
188 | error,
189 | fatal,
190 | info,
191 | // Re-export all from logfire
192 | Level,
193 | log,
194 | logfireApiConfig,
195 | LogfireAttributeScrubber,
196 | NoopAttributeScrubber,
197 | notice,
198 | reportError,
199 | resolveBaseUrl,
200 | resolveSendToLogfire,
201 | serializeAttributes,
202 | span,
203 | startSpan,
204 | trace,
205 | ULIDGenerator,
206 | warning,
207 | }
208 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/AttributeScrubber.ts:
--------------------------------------------------------------------------------
1 | // This file was generated by ChatGPT by asking it to port logfire/_internal/scrubbing.py to TypeScript.
2 |
3 | export type JsonPath = (number | string)[]
4 | export interface ScrubbedNote {
5 | matched_substring: string
6 | path: JsonPath
7 | }
8 |
9 | export interface ScrubMatch {
10 | path: JsonPath
11 | patternMatch: RegExpMatchArray
12 | value: unknown
13 | }
14 |
15 | export type ScrubCallback = (match: ScrubMatch) => unknown
16 |
17 | /**
18 | * Interface for attribute scrubbers that can process values and potentially
19 | * redact sensitive information.
20 | */
21 | export interface AttributeScrubber {
22 | /**
23 | * Scrubs a value recursively.
24 | * @param path The JSON path to this value.
25 | * @param value The value to scrub.
26 | * @returns A tuple: [scrubbedValue, scrubbedNotes]
27 | */
28 | scrubValue(path: JsonPath, value: unknown): readonly [unknown, ScrubbedNote[]]
29 | }
30 |
31 | /**
32 | * Base interface for attribute scrubbers with safe keys
33 | */
34 | export interface BaseScrubber extends AttributeScrubber {
35 | /**
36 | * List of keys that are considered safe and do not need scrubbing
37 | */
38 | SAFE_KEYS: string[]
39 |
40 | /**
41 | * Scrubs a value recursively.
42 | * @param path The JSON path to this value.
43 | * @param value The value to scrub.
44 | * @returns A tuple: [scrubbedValue, scrubbedNotes]
45 | */
46 | scrubValue(path: JsonPath, value: T): readonly [T, ScrubbedNote[]]
47 | }
48 |
49 | const DEFAULT_PATTERNS = [
50 | 'password',
51 | 'passwd',
52 | 'mysql_pwd',
53 | 'secret',
54 | 'auth(?!ors?\\b)',
55 | 'credential',
56 | 'private[._ -]?key',
57 | 'api[._ -]?key',
58 | 'session',
59 | 'cookie',
60 | 'csrf',
61 | 'xsrf',
62 | 'jwt',
63 | 'ssn',
64 | 'social[._ -]?security',
65 | 'credit[._ -]?card',
66 | ]
67 |
68 | // Should be kept roughly in sync with `logfire._internal.scrubbing.BaseScrubber.SAFE_KEYS`
69 | const SAFE_KEYS = new Set([
70 | 'code.filepath',
71 | 'code.function',
72 | 'code.lineno',
73 | 'db.plan',
74 | 'db.statement',
75 | 'exception.stacktrace',
76 | 'exception.type',
77 | 'http.method',
78 | 'http.route',
79 | 'http.scheme',
80 | 'http.status_code',
81 | 'http.target',
82 | 'http.url',
83 | 'logfire.json_schema',
84 | 'logfire.level_name',
85 | 'logfire.level_num',
86 | 'logfire.logger_name',
87 | 'logfire.msg',
88 | 'logfire.msg_template',
89 | 'logfire.null_args',
90 | 'logfire.package_versions',
91 | 'logfire.pending_parent_id',
92 | 'logfire.sample_rate',
93 | 'logfire.scrubbed',
94 | 'logfire.span_type',
95 | 'logfire.tags',
96 | 'schema.url',
97 | 'url.full',
98 | 'url.path',
99 | 'url.query',
100 | ])
101 |
102 | export class LogfireAttributeScrubber implements BaseScrubber {
103 | /**
104 | * List of keys that are considered safe and don't need scrubbing
105 | */
106 | SAFE_KEYS: string[] = Array.from(SAFE_KEYS)
107 | private _callback?: ScrubCallback
108 |
109 | private _pattern: RegExp
110 |
111 | constructor(patterns?: string[], callback?: ScrubCallback) {
112 | const allPatterns = [...DEFAULT_PATTERNS, ...(patterns ?? [])]
113 | this._pattern = new RegExp(allPatterns.join('|'), 'i')
114 | this._callback = callback
115 | }
116 |
117 | /**
118 | * Scrubs a value recursively using default patterns.
119 | * @param path The JSON path to this value.
120 | * @param value The value to scrub.
121 | * @returns A tuple: [scrubbedValue, scrubbedNotes]
122 | */
123 | scrubValue(path: JsonPath, value: T): readonly [T, ScrubbedNote[]] {
124 | const scrubbedNotes: ScrubbedNote[] = []
125 | const scrubbedValue = this.scrub(path, value, scrubbedNotes)
126 | return [scrubbedValue as T, scrubbedNotes] as const
127 | }
128 |
129 | private redact(path: JsonPath, value: unknown, match: RegExpMatchArray, notes: ScrubbedNote[]): unknown {
130 | // If callback is provided and returns a non-null value, use that
131 | if (this._callback) {
132 | const callbackResult = this._callback({ path, patternMatch: match, value })
133 | if (callbackResult !== null && callbackResult !== undefined) {
134 | return callbackResult
135 | }
136 | }
137 |
138 | const matchedSubstring = match[0]
139 | notes.push({ matched_substring: matchedSubstring, path })
140 | return `[Scrubbed due to '${matchedSubstring}']`
141 | }
142 |
143 | private scrub(path: JsonPath, value: unknown, notes: ScrubbedNote[]): unknown {
144 | if (typeof value === 'string') {
145 | // Check if the string matches the pattern
146 | const match = value.match(this._pattern)
147 | if (match) {
148 | // If the entire string is just the matched pattern, consider it safe.
149 | // e.g., if value == 'password', just leave it.
150 | if (!(match.index === 0 && match[0].length === value.length)) {
151 | // Try to parse as JSON
152 | try {
153 | const parsed = JSON.parse(value) as unknown
154 | // If parsed, scrub the parsed object
155 | const newVal = this.scrub(path, parsed, notes)
156 | return JSON.stringify(newVal)
157 | } catch {
158 | // Not JSON, redact directly
159 | return this.redact(path, value, match, notes)
160 | }
161 | }
162 | }
163 | return value
164 | } else if (Array.isArray(value)) {
165 | return value.map((v, i) => this.scrub([...path, i], v, notes))
166 | } else if (value && typeof value === 'object') {
167 | // Object
168 | const result: Record = {}
169 | for (const [k, v] of Object.entries(value)) {
170 | if (SAFE_KEYS.has(k) || ['boolean', 'number', 'undefined'].includes(typeof v) || v === null) {
171 | // Safe key or a primitive value, no scrubbing of the key itself.
172 | // (In the Python SDK we still scrub primitive values to be extra careful)
173 | result[k] = v
174 | } else {
175 | // Check key against the pattern
176 | const keyMatch = k.match(this._pattern)
177 | if (keyMatch) {
178 | // Key contains sensitive substring
179 | const redacted = this.redact([...path, k], v, keyMatch, notes)
180 | // If v is an object/array and got redacted to a string, we may want to consider if that's correct.
181 | // For simplicity, we just store the redacted string.
182 | result[k] = redacted
183 | } else {
184 | // Scrub the value recursively
185 | result[k] = this.scrub([...path, k], v, notes)
186 | }
187 | }
188 | }
189 | return result
190 | }
191 |
192 | return value
193 | }
194 | }
195 |
196 | /**
197 | * A no-op attribute scrubber that returns values unchanged.
198 | * Useful when you want to disable scrubbing entirely.
199 | */
200 | export class NoopAttributeScrubber implements BaseScrubber {
201 | /**
202 | * List of keys that are considered safe and don't need scrubbing
203 | */
204 | SAFE_KEYS: string[] = []
205 |
206 | /**
207 | * Returns the value unchanged with no scrubbing notes.
208 | * @param path The JSON path to this value.
209 | * @param value The value to return unchanged.
210 | * @returns A tuple: [originalValue, emptyNotes]
211 | */
212 | scrubValue(_path: JsonPath, value: T): readonly [T, ScrubbedNote[]] {
213 | return [value, []] as const
214 | }
215 | }
216 |
217 | /**
218 | * A singleton instance of NoopAttributeScrubber for convenience
219 | */
220 | export const NoopScrubber = new NoopAttributeScrubber()
221 |
--------------------------------------------------------------------------------
/packages/logfire-node/src/logfireConfig.ts:
--------------------------------------------------------------------------------
1 | import { DiagLogLevel } from '@opentelemetry/api'
2 | import { InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node'
3 | import { Instrumentation } from '@opentelemetry/instrumentation'
4 | import { MetricReader } from '@opentelemetry/sdk-metrics'
5 | import { IdGenerator, SpanProcessor } from '@opentelemetry/sdk-trace-base'
6 | import * as logfireApi from 'logfire'
7 |
8 | import { start } from './sdk'
9 |
10 | export interface AdvancedLogfireConfigOptions {
11 | /**
12 | * The logfire API base URL. Defaults to 'https://logfire-api.pydantic.dev/'
13 | */
14 | baseUrl?: string
15 | /**
16 | * The generator to use for generating trace IDs. Defaults to ULIDGenerator - https://github.com/ulid/spec.
17 | */
18 | idGenerator?: IdGenerator
19 | }
20 |
21 | export interface CodeSource {
22 | /**
23 | * The repository URL for the code e.g. https://github.com/pydantic/logfire
24 | */
25 | repository: string
26 | /**
27 | * The git revision of the code e.g. branch name, commit hash, tag name etc.
28 | */
29 | revision: string
30 | /**
31 | * The root path for the source code in the repository.
32 | *
33 | * If you run the code from the directory corresponding to the root of the repository, you can leave this blank.
34 | */
35 | rootPath?: string
36 | }
37 |
38 | export interface MetricsOptions {
39 | additionalReaders: MetricReader[]
40 | }
41 |
42 | export interface LogfireConfigOptions {
43 | /**
44 | * Additional span processors to be added to the OpenTelemetry SDK
45 | */
46 | additionalSpanProcessors?: SpanProcessor[]
47 | /**
48 | * Advanced configuration options
49 | */
50 | advanced?: AdvancedLogfireConfigOptions
51 | /**
52 | * Settings for the source code of the project.
53 | */
54 | codeSource?: CodeSource
55 | /**
56 | * Whether to log the spans to the console in addition to sending them to the Logfire API.
57 | */
58 | console?: boolean
59 | /**
60 | * Defines the available internal logging levels for the diagnostic logger.
61 | */
62 | diagLogLevel?: DiagLogLevel
63 | /**
64 | * Set to False to suppress extraction of incoming trace context. See [Unintentional Distributed Tracing](https://logfire.pydantic.dev/docs/how-to-guides/distributed-tracing/#unintentional-distributed-tracing) for more information.
65 | */
66 | distributedTracing?: boolean
67 | /**
68 | * The environment this service is running in, e.g. `staging` or `prod`. Sets the deployment.environment.name resource attribute. Useful for filtering within projects in the Logfire UI.
69 | * Defaults to the `LOGFIRE_ENVIRONMENT` environment variable.
70 | */
71 | environment?: string
72 | /**
73 | * Additional third-party instrumentations to use.
74 | */
75 | instrumentations?: Instrumentation[]
76 | /**
77 | * Set to False to disable sending all metrics, or provide a MetricsOptions object to configure metrics, e.g. additional metric readers.
78 | */
79 | metrics?: false | MetricsOptions
80 | /**
81 | * The node auto instrumentations to use. See [Node Auto Instrumentations](https://opentelemetry.io/docs/languages/js/libraries/#registration) for more information.
82 | */
83 | nodeAutoInstrumentations?: InstrumentationConfigMap
84 | /**
85 | * The otel scope to use for the logfire API. Defaults to 'logfire'.
86 | */
87 | otelScope?: string
88 | /**
89 | * Options for scrubbing sensitive data. Set to False to disable.
90 | */
91 | scrubbing?: false | logfireApi.ScrubbingOptions
92 | /**
93 | * Whether to send logs to logfire.dev.
94 | * Defaults to the `LOGFIRE_SEND_TO_LOGFIRE` environment variable if set, otherwise defaults to True. If if-token-present is provided, logs will only be sent if a token is present.
95 | */
96 | sendToLogfire?: 'if-token-present' | boolean
97 | /**
98 | * Name of this service.
99 | * Defaults to the `LOGFIRE_SERVICE_NAME` environment variable.
100 | */
101 | serviceName?: string
102 | /**
103 | * Version of this service.
104 | * Defaults to the `LOGFIRE_SERVICE_VERSION` environment variable.
105 | */
106 | serviceVersion?: string
107 | /**
108 | * The project token.
109 | * Defaults to the `LOGFIRE_TOKEN` environment variable.
110 | */
111 | token?: string
112 | }
113 |
114 | const DEFAULT_OTEL_SCOPE = 'logfire'
115 | const TRACE_ENDPOINT_PATH = 'v1/traces'
116 | const METRIC_ENDPOINT_PATH = 'v1/metrics'
117 | const DEFAULT_AUTO_INSTRUMENTATION_CONFIG: InstrumentationConfigMap = {
118 | // https://opentelemetry.io/docs/languages/js/libraries/#registration
119 | // This particular instrumentation creates a lot of noise on startup
120 | '@opentelemetry/instrumentation-fs': {
121 | enabled: false,
122 | },
123 | }
124 |
125 | export interface LogfireConfig {
126 | additionalSpanProcessors: SpanProcessor[]
127 | authorizationHeaders: Record
128 | baseUrl: string
129 | codeSource: CodeSource | undefined
130 | console: boolean | undefined
131 | deploymentEnvironment: string | undefined
132 | diagLogLevel?: DiagLogLevel
133 | distributedTracing: boolean
134 | idGenerator: IdGenerator
135 | instrumentations: Instrumentation[]
136 | metricExporterUrl: string
137 | metrics: false | MetricsOptions | undefined
138 | nodeAutoInstrumentations: InstrumentationConfigMap
139 | otelScope: string
140 | sendToLogfire: boolean
141 | serviceName: string | undefined
142 | serviceVersion: string | undefined
143 | token: string | undefined
144 | traceExporterUrl: string
145 | }
146 |
147 | const DEFAULT_LOGFIRE_CONFIG: LogfireConfig = {
148 | additionalSpanProcessors: [],
149 | authorizationHeaders: {},
150 | baseUrl: '',
151 | codeSource: undefined,
152 | console: false,
153 | deploymentEnvironment: undefined,
154 | diagLogLevel: undefined,
155 | distributedTracing: true,
156 | idGenerator: new logfireApi.ULIDGenerator(),
157 | instrumentations: [],
158 | metricExporterUrl: '',
159 | metrics: undefined,
160 | nodeAutoInstrumentations: DEFAULT_AUTO_INSTRUMENTATION_CONFIG,
161 | otelScope: DEFAULT_OTEL_SCOPE,
162 | sendToLogfire: false,
163 | serviceName: process.env.LOGFIRE_SERVICE_NAME,
164 | serviceVersion: process.env.LOGFIRE_SERVICE_VERSION,
165 | token: '',
166 | traceExporterUrl: '',
167 | }
168 |
169 | export const logfireConfig: LogfireConfig = DEFAULT_LOGFIRE_CONFIG
170 |
171 | export function configure(config: LogfireConfigOptions = {}) {
172 | const { otelScope, scrubbing, ...cnf } = config
173 |
174 | const env = process.env
175 |
176 | if (otelScope !== undefined || scrubbing !== undefined) {
177 | logfireApi.configureLogfireApi({ otelScope, scrubbing })
178 | }
179 |
180 | const token = cnf.token ?? env.LOGFIRE_TOKEN
181 | const sendToLogfire = logfireApi.resolveSendToLogfire(process.env, cnf.sendToLogfire, token)
182 | const baseUrl = !sendToLogfire || !token ? '' : logfireApi.resolveBaseUrl(process.env, cnf.advanced?.baseUrl, token)
183 | const console = 'console' in cnf ? cnf.console : env.LOGFIRE_CONSOLE === 'true'
184 |
185 | Object.assign(logfireConfig, {
186 | additionalSpanProcessors: cnf.additionalSpanProcessors ?? [],
187 | authorizationHeaders: {
188 | Authorization: token ?? '',
189 | },
190 | baseUrl,
191 | codeSource: cnf.codeSource,
192 | console,
193 | deploymentEnvironment: cnf.environment ?? env.LOGFIRE_ENVIRONMENT,
194 | diagLogLevel: cnf.diagLogLevel,
195 | distributedTracing: resolveDistributedTracing(cnf.distributedTracing),
196 | idGenerator: cnf.advanced?.idGenerator ?? new logfireApi.ULIDGenerator(),
197 | instrumentations: cnf.instrumentations ?? [],
198 | metricExporterUrl: `${baseUrl}/${METRIC_ENDPOINT_PATH}`,
199 | metrics: cnf.metrics,
200 | nodeAutoInstrumentations: cnf.nodeAutoInstrumentations ?? DEFAULT_AUTO_INSTRUMENTATION_CONFIG,
201 | sendToLogfire,
202 | serviceName: cnf.serviceName ?? env.LOGFIRE_SERVICE_NAME,
203 | serviceVersion: cnf.serviceVersion ?? env.LOGFIRE_SERVICE_VERSION,
204 | token,
205 | traceExporterUrl: `${baseUrl}/${TRACE_ENDPOINT_PATH}`,
206 | })
207 |
208 | start()
209 | }
210 |
211 | function resolveDistributedTracing(option: LogfireConfigOptions['distributedTracing']) {
212 | const envDistributedTracing = process.env.LOGFIRE_DISTRIBUTED_TRACING
213 | return (option ?? envDistributedTracing === undefined) ? true : envDistributedTracing === 'true'
214 | }
215 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable perfectionist/sort-objects */
2 | import { Span, SpanStatusCode, context as TheContextAPI, trace as TheTraceAPI } from '@opentelemetry/api'
3 | import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_STACKTRACE } from '@opentelemetry/semantic-conventions'
4 |
5 | import * as AttributeScrubbingExports from './AttributeScrubber'
6 | import {
7 | ATTRIBUTES_LEVEL_KEY,
8 | ATTRIBUTES_MESSAGE_KEY,
9 | ATTRIBUTES_MESSAGE_TEMPLATE_KEY,
10 | ATTRIBUTES_SPAN_TYPE_KEY,
11 | ATTRIBUTES_TAGS_KEY,
12 | } from './constants'
13 | import { logfireFormatWithExtras } from './formatter'
14 | import { logfireApiConfig, serializeAttributes } from './logfireApiConfig'
15 | import * as logfireApiConfigExports from './logfireApiConfig'
16 | import * as ULIDGeneratorExports from './ULIDGenerator'
17 |
18 | export * from './AttributeScrubber'
19 | export { configureLogfireApi, logfireApiConfig, resolveBaseUrl, resolveSendToLogfire } from './logfireApiConfig'
20 | export type { LogfireApiConfig, LogfireApiConfigOptions, ScrubbingOptions } from './logfireApiConfig'
21 | export { serializeAttributes } from './serializeAttributes'
22 | export * from './ULIDGenerator'
23 |
24 | export const Level = {
25 | Trace: 1 as const,
26 | Debug: 5 as const,
27 | Info: 9 as const,
28 | Notice: 10 as const,
29 | Warning: 13 as const,
30 | Error: 17 as const,
31 | Fatal: 21 as const,
32 | }
33 |
34 | export type LogFireLevel = (typeof Level)[keyof typeof Level]
35 |
36 | export interface LogOptions {
37 | /**
38 | * The log level for the span.
39 | * Defaults to Level.Info.
40 | */
41 | level?: LogFireLevel
42 | /**
43 | * Set to true to indicate that this span is a log. logs don't have child spans.
44 | */
45 | log?: true
46 | /**
47 | * Set a span started with `startSpan` as parentSpan to create a child span.
48 | */
49 | parentSpan?: Span
50 | /**
51 | * Tags to add to the span.
52 | */
53 | tags?: string[]
54 | }
55 |
56 | /**
57 | * Starts a new Span without setting it on context.
58 | * This method does NOT modify the current Context.
59 | * You need to manually call `span.end()` to finish the span.
60 | */
61 | export function startSpan(
62 | msgTemplate: string,
63 | attributes: Record = {},
64 | { log, tags = [], level = Level.Info, parentSpan }: LogOptions = {}
65 | ): Span {
66 | const { formattedMessage, extraAttributes, newTemplate } = logfireFormatWithExtras(msgTemplate, attributes, logfireApiConfig.scrubber)
67 |
68 | const context = parentSpan ? TheTraceAPI.setSpan(TheContextAPI.active(), parentSpan) : TheContextAPI.active()
69 | const span = logfireApiConfig.tracer.startSpan(
70 | msgTemplate,
71 | {
72 | attributes: {
73 | ...serializeAttributes({ ...attributes, ...extraAttributes }),
74 | [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: newTemplate,
75 | [ATTRIBUTES_MESSAGE_KEY]: formattedMessage,
76 | [ATTRIBUTES_LEVEL_KEY]: level,
77 | [ATTRIBUTES_TAGS_KEY]: Array.from(new Set(tags).values()),
78 | [ATTRIBUTES_SPAN_TYPE_KEY]: log ? 'log' : 'span',
79 | },
80 | },
81 | context
82 | )
83 |
84 | return span
85 | }
86 |
87 | type SpanCallback = (activeSpan: Span) => R
88 | type SpanArgsVariant1 = [Record, LogOptions, SpanCallback]
89 | type SpanArgsVariant2 = [
90 | { attributes?: Record; callback: SpanCallback; level?: LogFireLevel; parentSpan?: Span; tags?: string[] },
91 | ]
92 |
93 | /**
94 | * Starts a new Span and calls the given function passing it the
95 | * created span as first argument.
96 | * Additionally the new span gets set in context and this context is activated within the execution of the function.
97 | * The span will be ended automatically after the function call.
98 | */
99 | export function span(msgTemplate: string, options: SpanArgsVariant2[0]): R
100 | // eslint-disable-next-line no-redeclare
101 | export function span(msgTemplate: string, attributes: Record, options: LogOptions, callback: (span: Span) => R): R
102 | // eslint-disable-next-line no-redeclare
103 | export function span(msgTemplate: string, ...args: SpanArgsVariant1 | SpanArgsVariant2): R {
104 | let attributes: Record = {}
105 | let level: LogFireLevel = Level.Info
106 | let tags: string[] = []
107 | let callback!: SpanCallback
108 | let parentSpan: Span | undefined
109 | if (args.length === 1) {
110 | attributes = args[0].attributes ?? {}
111 | level = args[0].level ?? Level.Info
112 | tags = args[0].tags ?? []
113 | callback = args[0].callback
114 | parentSpan = args[0].parentSpan
115 | } else {
116 | attributes = args[0]
117 | level = args[1].level ?? Level.Info
118 | tags = args[1].tags ?? []
119 | parentSpan = args[1].parentSpan
120 | callback = args[2]
121 | }
122 |
123 | const { formattedMessage, extraAttributes, newTemplate } = logfireFormatWithExtras(msgTemplate, attributes, logfireApiConfig.scrubber)
124 |
125 | const context = parentSpan ? TheTraceAPI.setSpan(TheContextAPI.active(), parentSpan) : TheContextAPI.active()
126 | return logfireApiConfig.tracer.startActiveSpan(
127 | msgTemplate,
128 | {
129 | attributes: {
130 | ...serializeAttributes({ ...attributes, ...extraAttributes }),
131 | [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: newTemplate,
132 | [ATTRIBUTES_MESSAGE_KEY]: formattedMessage,
133 | [ATTRIBUTES_LEVEL_KEY]: level,
134 | [ATTRIBUTES_TAGS_KEY]: Array.from(new Set(tags).values()),
135 | },
136 | },
137 | context,
138 | (span: Span) => {
139 | const result = callback(span)
140 |
141 | // we need this clunky detection because of zone.js promises
142 | if (typeof result === 'object' && result !== null && 'finally' in result && typeof result.finally === 'function') {
143 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call
144 | result.finally(() => {
145 | span.end()
146 | })
147 | } else {
148 | span.end()
149 | }
150 | return result
151 | }
152 | )
153 | }
154 |
155 | export function log(message: string, attributes: Record = {}, options: LogOptions = {}) {
156 | startSpan(message, attributes, { ...options, log: true }).end()
157 | }
158 |
159 | export function debug(message: string, attributes: Record = {}, options: LogOptions = {}) {
160 | log(message, attributes, { ...options, level: Level.Debug })
161 | }
162 |
163 | export function info(message: string, attributes: Record = {}, options: LogOptions = {}) {
164 | log(message, attributes, { ...options, level: Level.Info })
165 | }
166 |
167 | export function trace(message: string, attributes: Record = {}, options: LogOptions = {}) {
168 | log(message, attributes, { ...options, level: Level.Trace })
169 | }
170 |
171 | export function error(message: string, attributes: Record = {}, options: LogOptions = {}) {
172 | log(message, attributes, { ...options, level: Level.Error })
173 | }
174 |
175 | export function fatal(message: string, attributes: Record = {}, options: LogOptions = {}) {
176 | log(message, attributes, { ...options, level: Level.Fatal })
177 | }
178 |
179 | export function notice(message: string, attributes: Record = {}, options: LogOptions = {}) {
180 | log(message, attributes, { ...options, level: Level.Notice })
181 | }
182 |
183 | export function warning(message: string, attributes: Record = {}, options: LogOptions = {}) {
184 | log(message, attributes, { ...options, level: Level.Warning })
185 | }
186 |
187 | /**
188 | * Use this method to report an error to Logfire.
189 | * Captures the error stack trace and message in the respective semantic attributes and sets the correct level and status.
190 | */
191 | export function reportError(message: string, error: Error, extraAttributes: Record = {}) {
192 | const span = startSpan(
193 | message,
194 | {
195 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
196 | [ATTR_EXCEPTION_MESSAGE]: error.message ?? 'error',
197 | [ATTR_EXCEPTION_STACKTRACE]: error.stack,
198 | ...extraAttributes,
199 | },
200 | {
201 | level: Level.Error,
202 | }
203 | )
204 |
205 | span.recordException(error)
206 | span.setStatus({ code: SpanStatusCode.ERROR })
207 | span.end()
208 | }
209 |
210 | const defaultExport = {
211 | ...AttributeScrubbingExports,
212 | ...ULIDGeneratorExports,
213 | ...logfireApiConfigExports,
214 |
215 | serializeAttributes,
216 | Level,
217 | startSpan,
218 | span,
219 | log,
220 | debug,
221 | info,
222 | trace,
223 | error,
224 | fatal,
225 | notice,
226 | warning,
227 | reportError,
228 | }
229 |
230 | export default defaultExport
231 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/formatter.ts:
--------------------------------------------------------------------------------
1 | import { BaseScrubber, ScrubbedNote } from './AttributeScrubber'
2 | import { ATTRIBUTES_SCRUBBED_KEY, MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT } from './constants'
3 |
4 | // TypeScript equivalent of Python's TypedDict
5 | interface LiteralChunk {
6 | type: 'lit'
7 | value: string
8 | }
9 |
10 | interface ArgChunk {
11 | spec?: string
12 | type: 'arg'
13 | value: string
14 | }
15 |
16 | class KnownFormattingError extends Error {
17 | constructor(message: string) {
18 | super(message)
19 | this.name = 'KnownFormattingError'
20 | }
21 | }
22 |
23 | class ChunksFormatter {
24 | // Internal regex to parse format strings (similar to Python's Formatter.parse)
25 | private parseRegex = /(\{\{)|(\}\})|(\{([^{}]*)(?::([^{}]*))?\})/g
26 |
27 | chunks(
28 | formatString: string,
29 | record: Record,
30 | scrubber: BaseScrubber
31 | ): [(ArgChunk | LiteralChunk)[], Record, string] {
32 | // TypeScript equivalent doesn't need f-string introspection as JavaScript template literals
33 | // are evaluated before the function is called
34 |
35 | const [chunks, extraAttrs] = this.vformatChunks(formatString, record, scrubber)
36 |
37 | // In TypeScript/JavaScript we don't need to handle f-strings separately
38 | return [chunks, extraAttrs, formatString]
39 | }
40 |
41 | // Format a single field value
42 | formatField(value: unknown, formatSpec: string): string {
43 | // Very simplified version - TypeScript doesn't have Python's rich formatting system
44 | if (!formatSpec) {
45 | return String(value)
46 | }
47 |
48 | // Simple number formatting for demonstration
49 | if (typeof value === 'number') {
50 | if (formatSpec.includes('.')) {
51 | const [, precision] = formatSpec.split('.') as [string, string]
52 | return value.toFixed(parseInt(precision, 10))
53 | }
54 | }
55 |
56 | // Default to string conversion
57 | return String(value)
58 | }
59 |
60 | // Equivalent to Python's getField method
61 | getField(fieldName: string, record: Record): [unknown, string] {
62 | if (fieldName.includes('.') || fieldName.includes('[')) {
63 | // Handle nested field access like "a.b" or "a[b]"
64 | try {
65 | // Simple nested property access (this is a simplification)
66 | const parts = fieldName.split('.')
67 | let obj = record[parts[0] ?? '']
68 | for (let i = 1; i < parts.length; i++) {
69 | const key = parts[i] ?? ''
70 | if (key in record) {
71 | obj = record[key]
72 | } else {
73 | throw new KnownFormattingError(`The field ${fieldName} is not an object.`)
74 | }
75 | }
76 | return [obj, parts[0] ?? '']
77 | } catch {
78 | // Try getting the whole thing from object
79 | if (fieldName in record) {
80 | return [record[fieldName], fieldName]
81 | }
82 | throw new KnownFormattingError(`The field ${fieldName} is not defined.`)
83 | }
84 | } else {
85 | // Simple field access
86 | if (fieldName in record) {
87 | return [record[fieldName], fieldName]
88 | }
89 | throw new KnownFormattingError(`The field ${fieldName} is not defined.`)
90 | }
91 | }
92 |
93 | parse(formatString: string): [string, null | string, null | string, null | string][] {
94 | const result: [string, null | string, null | string, null | string][] = []
95 | let lastIndex = 0
96 | let literalText = ''
97 |
98 | let match: null | RegExpExecArray
99 | while ((match = this.parseRegex.exec(formatString)) !== null) {
100 | const [fullMatch, doubleLBrace, doubleRBrace, curlyContent, fieldName, formatSpec] = match
101 |
102 | // Get literal text before the match
103 | const precedingText = formatString.substring(lastIndex, match.index)
104 | literalText += precedingText
105 |
106 | if (doubleLBrace) {
107 | // {{ is escaped to {
108 | literalText += '{'
109 | } else if (doubleRBrace) {
110 | // }} is escaped to }
111 | literalText += '}'
112 | } else if (curlyContent) {
113 | // Found a field, add the accumulated literal text and the field info
114 | result.push([literalText, fieldName ?? null, formatSpec ?? null, null])
115 | literalText = ''
116 | }
117 |
118 | lastIndex = match.index + fullMatch.length
119 | }
120 |
121 | // Add any remaining literal text
122 | if (lastIndex < formatString.length) {
123 | literalText += formatString.substring(lastIndex)
124 | }
125 |
126 | if (literalText) {
127 | result.push([literalText, null, null, null])
128 | }
129 |
130 | return result
131 | }
132 |
133 | private cleanValue(fieldName: string, value: string, scrubber: BaseScrubber): [string, ScrubbedNote[]] {
134 | // Scrub before truncating so the scrubber can see the full value
135 | if (scrubber.SAFE_KEYS.includes(fieldName)) {
136 | return [truncateString(value, MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT), []]
137 | }
138 |
139 | const [cleanValue, scrubbed] = scrubber.scrubValue(['message', fieldName], value)
140 |
141 | return [truncateString(cleanValue, MESSAGE_FORMATTED_VALUE_LENGTH_LIMIT), scrubbed]
142 | }
143 |
144 | private vformatChunks(
145 | formatString: string,
146 | record: Record,
147 | scrubber: BaseScrubber,
148 | recursionDepth = 2
149 | ): [(ArgChunk | LiteralChunk)[], Record] {
150 | if (recursionDepth < 0) {
151 | throw new KnownFormattingError('Max format spec recursion exceeded')
152 | }
153 |
154 | const result: (ArgChunk | LiteralChunk)[] = []
155 | const scrubbed: ScrubbedNote[] = []
156 |
157 | for (const [literalText, fieldName, formatSpec] of this.parse(formatString)) {
158 | // Output the literal text
159 | if (literalText) {
160 | result.push({ type: 'lit', value: literalText })
161 | }
162 |
163 | // If there's a field, output it
164 | if (fieldName !== null) {
165 | // Handle markup and formatting
166 | if (fieldName === '') {
167 | throw new KnownFormattingError('Empty curly brackets `{}` are not allowed. A field name is required.')
168 | }
169 |
170 | // Handle debug format like "{field=}"
171 | let actualFieldName = fieldName
172 | if (fieldName.endsWith('=')) {
173 | const lastResult = result[result.length - 1] ?? null
174 | if (lastResult !== null && lastResult.type === 'lit') {
175 | lastResult.value += fieldName
176 | } else {
177 | result.push({ type: 'lit', value: fieldName })
178 | }
179 | actualFieldName = fieldName.slice(0, -1)
180 | }
181 |
182 | // Get the object referenced by the field name
183 | let obj
184 | try {
185 | ;[obj] = this.getField(actualFieldName, record)
186 | } catch (err) {
187 | if (err instanceof KnownFormattingError) {
188 | throw err
189 | }
190 | throw new KnownFormattingError(`Error getting field ${actualFieldName}: ${String(err)}`)
191 | }
192 |
193 | // Format the field value
194 | let formattedValue
195 | try {
196 | formattedValue = this.formatField(obj, formatSpec ?? '')
197 | } catch (err) {
198 | throw new KnownFormattingError(`Error formatting field ${actualFieldName}: ${String(err)}`)
199 | }
200 |
201 | // Clean and scrub the value
202 | const [cleanValue, valueScrubbed] = this.cleanValue(actualFieldName, formattedValue, scrubber)
203 | scrubbed.push(...valueScrubbed)
204 |
205 | const argChunk: ArgChunk = { type: 'arg', value: cleanValue }
206 | if (formatSpec) {
207 | argChunk.spec = formatSpec
208 | }
209 | result.push(argChunk)
210 | }
211 | }
212 |
213 | const extraAttrs = scrubbed.length > 0 ? { [ATTRIBUTES_SCRUBBED_KEY]: scrubbed } : {}
214 | return [result, extraAttrs]
215 | }
216 | }
217 |
218 | // Create singleton instance
219 | export const chunksFormatter = new ChunksFormatter()
220 |
221 | /**
222 | * Format a string with additional information about attributes and templates
223 | */
224 | export function logfireFormatWithExtras(
225 | formatString: string,
226 | record: Record,
227 | scrubber: BaseScrubber
228 | ): {
229 | extraAttributes: Record
230 | formattedMessage: string
231 | newTemplate: string
232 | } {
233 | try {
234 | const [chunks, extraAttributes, newTemplate] = chunksFormatter.chunks(formatString, record, scrubber)
235 |
236 | const formattedMessage = chunks.map((chunk) => chunk.value).join('')
237 | return {
238 | extraAttributes,
239 | formattedMessage,
240 | newTemplate,
241 | }
242 | } catch (err) {
243 | if (err instanceof KnownFormattingError) {
244 | console.warn(`Formatting error: ${err.message}`)
245 | } else {
246 | console.error('Unexpected error during formatting:', err)
247 | }
248 |
249 | // Formatting failed, use the original format string as the message
250 | return {
251 | extraAttributes: {},
252 | formattedMessage: formatString,
253 | newTemplate: formatString,
254 | }
255 | }
256 | }
257 |
258 | /**
259 | * Truncates a string if it exceeds the specified maximum length.
260 | *
261 | * @param str The string to truncate
262 | * @param maxLength The maximum allowed length
263 | * @returns The truncated string
264 | */
265 | export function truncateString(str: string, maxLength: number): string {
266 | if (str.length <= maxLength) {
267 | return str
268 | }
269 |
270 | // Truncate and add ellipsis
271 | return str.substring(0, maxLength - 3) + '...'
272 | }
273 |
--------------------------------------------------------------------------------