├── 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 | --------------------------------------------------------------------------------