diff --git a/.changeset/fix-default-export-vite-dts-build.md b/.changeset/fix-default-export-vite-dts-build.md new file mode 100644 index 0000000..9808b9d --- /dev/null +++ b/.changeset/fix-default-export-vite-dts-build.md @@ -0,0 +1,8 @@ +--- +'@pydantic/logfire-api': minor +'@pydantic/logfire-browser': minor +'@pydantic/logfire-cf-workers': minor +'logfire': minor +--- + +Add default export to packages. Using the default import is equivalent to the star import. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b5df8f2 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(cat:*)", + "WebSearch", + "Bash(npm run build)", + "Bash(npm run build:*)", + "Bash(npx changeset:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore index 9620973..45b95d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ node_modules packages/*/*.tgz .turbo .env -CLAUDE.md scratch/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..74f144e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,185 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +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. + +## Repository Structure + +This is an **npm workspace monorepo** managed with **Turborepo**: + +- `packages/logfire` - Main Node.js SDK with automatic OpenTelemetry instrumentation +- `packages/logfire-api` - Core API package that can be used standalone for manual tracing (no auto-instrumentation) +- `packages/logfire-cf-workers` - Cloudflare Workers integration +- `packages/logfire-browser` - Browser/web SDK +- `packages/tooling-config` - Shared build and linting configuration +- `examples/` - Working examples for various platforms (Express, Next.js, Deno, Cloudflare Workers, etc.) + +## Core Architecture + +### Package Relationships + +- `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 +- `logfire` (Node.js SDK) depends on `logfire-api` and adds automatic instrumentation via `@opentelemetry/auto-instrumentations-node` +- `logfire-cf-workers` depends on `logfire-api` and adds Cloudflare Workers-specific instrumentation +- `logfire-browser` depends on `logfire-api` and adds browser-specific instrumentation + +### Key Concepts + +**Trace API** (`logfire-api`): + +- Provides convenience wrappers around OpenTelemetry spans with log levels (trace, debug, info, notice, warn, error, fatal) +- Uses message template formatting with attribute extraction (see `formatter.ts`) +- Uses ULID for trace ID generation (see `ULIDGenerator.ts`) +- Supports attribute scrubbing for sensitive data (see `AttributeScrubber.ts`) + +**Configuration** (`logfire` package): + +- `configure()` function in `logfireConfig.ts` handles SDK initialization +- Configuration can be provided programmatically or via environment variables: + - `LOGFIRE_TOKEN` - Authentication token + - `LOGFIRE_SERVICE_NAME` - Service name + - `LOGFIRE_SERVICE_VERSION` - Service version + - `LOGFIRE_ENVIRONMENT` - Deployment environment + - `LOGFIRE_CONSOLE` - Enable console output + - `LOGFIRE_SEND_TO_LOGFIRE` - Toggle sending to Logfire backend + - `LOGFIRE_DISTRIBUTED_TRACING` - Enable/disable trace context propagation + +**Span Creation**: + +- `startSpan()` - Creates a span without setting it on context (manual mode) +- `span()` - Creates a span, executes a callback, and auto-ends the span (recommended) +- `info()`, `debug()`, `error()`, etc. - Convenience methods that create log-type spans +- All spans use message templates with attribute extraction (e.g., `"User {user_id} logged in"`) + +## Common Commands + +### Development Setup + +```bash +npm install +``` + +### Building + +```bash +# Build all packages +npm run build + +# Build in watch mode (for development) +npm run dev +``` + +### Testing + +```bash +# Run all tests +npm run test + +# Run tests for a specific package +cd packages/logfire && npm test +``` + +### Linting and Type Checking + +```bash +# Run both typecheck and lint across all packages +npm run ci + +# Just linting +turbo lint + +# Just type checking +turbo typecheck +``` + +### Working with Examples + +Start an example to test changes: + +```bash +# Navigate to an example +cd examples/node # or express, nextjs, cf-worker, etc. + +# Install dependencies (if needed) +npm install + +# Run the example (check the example's package.json for scripts) +npm start # or npm run dev +``` + +### Changesets (Version Management) + +This project uses Changesets for version management: + +```bash +# Add a changeset when making changes +npm run changeset-add + +# Publish packages (maintainers only) +npm run release +``` + +### Running a Single Test + +```bash +# Navigate to the package +cd packages/logfire-api + +# Run vitest with a filter +npm test -- -t "test name pattern" +``` + +## Development Workflow + +1. Make changes in `packages/` source code +2. Run `npm run build` to rebuild packages (or `npm run dev` for watch mode) +3. Test changes using examples in `examples/` directory +4. Run `npm run ci` to ensure linting and type checking pass +5. Add a changeset if the changes warrant a version bump: `npm run changeset-add` + +## Important Implementation Details + +### Message Template Formatting + +The `logfireFormatWithExtras()` function in `formatter.ts` extracts attributes from message templates. For example: + +- `"User {user_id} logged in"` with `{ user_id: 123 }` becomes formatted message `"User 123 logged in"` +- Extracted attributes are stored with special keys and used by the Logfire backend + +### Attribute Scrubbing + +Sensitive data scrubbing is handled in `AttributeScrubber.ts`. By default, it redacts common sensitive patterns (passwords, tokens, API keys, etc.) using regex patterns. + +### Span Types + +Spans have a `logfire.span_type` attribute: + +- `"log"` - Point-in-time events (no child spans expected) +- `"span"` - Duration-based traces (can have child spans) + +### ID Generation + +The SDK uses ULID (Universally Unique Lexicographically Sortable Identifier) for trace IDs by default, which provides time-ordered IDs for better performance. + +### Build System + +- Uses Vite for building packages (see individual `vite.config.ts` files) +- Shared Vite config is in `packages/tooling-config/vite-config.ts` +- Outputs both ESM (`.js`) and CommonJS (`.cjs`) formats with corresponding TypeScript definitions + +## Testing Notes + +- Tests use Vitest +- Some packages have minimal tests (`--passWithNoTests` flag in package.json) +- Test files are located alongside source files with `.test.ts` extension + +## Node Version + +The project requires **Node.js 22** (see `engines` in root package.json). + +## Package Manager + +Uses **npm 10.9.2** (enforced via `packageManager` field). diff --git a/agent/prompts/add-default-export.md b/agent/prompts/add-default-export.md new file mode 100644 index 0000000..88acb76 --- /dev/null +++ b/agent/prompts/add-default-export.md @@ -0,0 +1,21 @@ +# Default export addition + +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: + +```ts +import * as logfire from 'logfire'; +``` + +```ts +import logfire from 'logfire'; +``` + +Implement this for every package in the monorepo. Do not touch the examples. + +## Details + +Explicitly construct the default export object using the current imports. This should happen in the index.ts files. + +## Testing + +Test this feature by rebuilding the packages and verifying the resulting bundles. diff --git a/examples/cf-tail-worker/package.json b/examples/cf-tail-worker/package.json index 99ee4b7..f32c199 100644 --- a/examples/cf-tail-worker/package.json +++ b/examples/cf-tail-worker/package.json @@ -6,7 +6,6 @@ "deploy": "wrangler deploy", "dev": "wrangler dev", "start": "wrangler dev", - "test": "vitest", "cf-typegen": "wrangler types" }, "devDependencies": { diff --git a/package.json b/package.json index 701fc37..e0e8d9a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "test": "turbo test", "release": "turbo build && npx @changesets/cli publish", "changeset-add": "npx @changesets/cli add", - "ci": "turbo typecheck lint" + "ci": "turbo typecheck lint test" }, "keywords": [], "author": "", diff --git a/packages/logfire-api/package.json b/packages/logfire-api/package.json index d930116..54ba4c4 100644 --- a/packages/logfire-api/package.json +++ b/packages/logfire-api/package.json @@ -51,7 +51,7 @@ "typecheck": "tsc", "prepack": "cp ../../LICENSE .", "postpack": "rm LICENSE", - "test": "vitest" + "test": "vitest run" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/logfire-api/src/index.test.ts b/packages/logfire-api/src/index.test.ts index e7fe85d..cec8ae4 100644 --- a/packages/logfire-api/src/index.test.ts +++ b/packages/logfire-api/src/index.test.ts @@ -1,7 +1,13 @@ import { trace } from '@opentelemetry/api' import { beforeEach, describe, expect, test, vi } from 'vitest' -import { ATTRIBUTES_LEVEL_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY } from './constants' +import { + ATTRIBUTES_LEVEL_KEY, + ATTRIBUTES_MESSAGE_KEY, + ATTRIBUTES_MESSAGE_TEMPLATE_KEY, + ATTRIBUTES_SPAN_TYPE_KEY, + ATTRIBUTES_TAGS_KEY, +} from './constants' import { info } from './index' vi.mock('@opentelemetry/api', () => { @@ -35,10 +41,11 @@ describe('info', () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(tracer.startSpan).toBeCalledWith( - 'aha 1', + 'aha {i}', { attributes: { [ATTRIBUTES_LEVEL_KEY]: 9, + [ATTRIBUTES_MESSAGE_KEY]: 'aha 1', [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: 'aha {i}', [ATTRIBUTES_SPAN_TYPE_KEY]: 'log', [ATTRIBUTES_TAGS_KEY]: [], @@ -55,10 +62,11 @@ describe('info', () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(tracer.startSpan).toBeCalledWith( - 'aha 1', + 'aha {i}', { attributes: { [ATTRIBUTES_LEVEL_KEY]: 9, + [ATTRIBUTES_MESSAGE_KEY]: 'aha 1', [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: 'aha {i}', [ATTRIBUTES_SPAN_TYPE_KEY]: 'log', [ATTRIBUTES_TAGS_KEY]: [], diff --git a/packages/logfire-api/src/index.ts b/packages/logfire-api/src/index.ts index 2328edf..d6a89aa 100644 --- a/packages/logfire-api/src/index.ts +++ b/packages/logfire-api/src/index.ts @@ -2,6 +2,7 @@ import { Span, SpanStatusCode, context as TheContextAPI, trace as TheTraceAPI } from '@opentelemetry/api' import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_STACKTRACE } from '@opentelemetry/semantic-conventions' +import * as AttributeScrubbingExports from './AttributeScrubber' import { ATTRIBUTES_LEVEL_KEY, ATTRIBUTES_MESSAGE_KEY, @@ -10,22 +11,16 @@ import { ATTRIBUTES_TAGS_KEY, } from './constants' import { logfireFormatWithExtras } from './formatter' -import { logfireApiConfig, ScrubbingOptions, serializeAttributes } from './logfireApiConfig' +import { logfireApiConfig, serializeAttributes } from './logfireApiConfig' +import * as logfireApiConfigExports from './logfireApiConfig' +import * as ULIDGeneratorExports from './ULIDGenerator' export * from './AttributeScrubber' export { configureLogfireApi, logfireApiConfig, resolveBaseUrl, resolveSendToLogfire } from './logfireApiConfig' -export type { ScrubbingOptions } from './logfireApiConfig' +export type { LogfireApiConfig, LogfireApiConfigOptions, ScrubbingOptions } from './logfireApiConfig' export { serializeAttributes } from './serializeAttributes' export * from './ULIDGenerator' -export interface LogfireApiConfigOptions { - otelScope?: string - /** - * Options for scrubbing sensitive data. Set to False to disable. - */ - scrubbing?: false | ScrubbingOptions -} - export const Level = { Trace: 1 as const, Debug: 5 as const, @@ -211,3 +206,25 @@ export function reportError(message: string, error: Error, extraAttributes: Reco span.setStatus({ code: SpanStatusCode.ERROR }) span.end() } + +const defaultExport = { + ...AttributeScrubbingExports, + ...ULIDGeneratorExports, + ...logfireApiConfigExports, + + serializeAttributes, + Level, + startSpan, + span, + log, + debug, + info, + trace, + error, + fatal, + notice, + warning, + reportError, +} + +export default defaultExport diff --git a/packages/logfire-browser/package.json b/packages/logfire-browser/package.json index d31ec1e..332317b 100644 --- a/packages/logfire-browser/package.json +++ b/packages/logfire-browser/package.json @@ -50,7 +50,7 @@ "typecheck": "tsc", "prepack": "cp ../../LICENSE .", "postpack": "rm LICENSE", - "test": "vitest --passWithNoTests" + "test": "vitest run --passWithNoTests" }, "dependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/logfire-browser/src/index.ts b/packages/logfire-browser/src/index.ts index 95e60f6..c43f2f1 100644 --- a/packages/logfire-browser/src/index.ts +++ b/packages/logfire-browser/src/index.ts @@ -19,8 +19,29 @@ import { ATTR_BROWSER_PLATFORM, ATTR_DEPLOYMENT_ENVIRONMENT_NAME, } from '@opentelemetry/semantic-conventions/incubating' -import * as logfireApi from '@pydantic/logfire-api' -import { ULIDGenerator } from '@pydantic/logfire-api' +import { + configureLogfireApi, + debug, + error, + fatal, + info, + Level, + log, + logfireApiConfig, + LogfireAttributeScrubber, + NoopAttributeScrubber, + notice, + reportError, + resolveBaseUrl, + resolveSendToLogfire, + type ScrubbingOptions, + serializeAttributes, + span, + startSpan, + trace, + ULIDGenerator, + warning, +} from '@pydantic/logfire-api' import { LogfireSpanProcessor } from './LogfireSpanProcessor' import { OTLPTraceExporterWithDynamicHeaders } from './OTLPTraceExporterWithDynamicHeaders' @@ -60,7 +81,7 @@ export interface LogfireConfigOptions { /** * Options for scrubbing sensitive data. Set to False to disable. */ - scrubbing?: false | logfireApi.ScrubbingOptions + scrubbing?: false | ScrubbingOptions /** * Name of this service. @@ -99,7 +120,7 @@ export function configure(options: LogfireConfigOptions) { } if (options.scrubbing !== undefined) { - logfireApi.configureLogfireApi({ scrubbing: options.scrubbing }) + configureLogfireApi({ scrubbing: options.scrubbing }) } const resource = resourceFromAttributes({ @@ -157,3 +178,30 @@ export function configure(options: LogfireConfigOptions) { diag.info('logfire-browser: shut down complete') } } + +// Create default export by listing all exports explicitly +export default { + configure, + configureLogfireApi, + debug, + DiagLogLevel, + error, + fatal, + info, + // Re-export all from @pydantic/logfire-api + Level, + log, + logfireApiConfig, + LogfireAttributeScrubber, + NoopAttributeScrubber, + notice, + reportError, + resolveBaseUrl, + resolveSendToLogfire, + serializeAttributes, + span, + startSpan, + trace, + ULIDGenerator, + warning, +} diff --git a/packages/logfire-cf-workers/src/exportTailEventsToLogfire.ts b/packages/logfire-cf-workers/src/exportTailEventsToLogfire.ts index 9480792..7803962 100644 --- a/packages/logfire-cf-workers/src/exportTailEventsToLogfire.ts +++ b/packages/logfire-cf-workers/src/exportTailEventsToLogfire.ts @@ -1,7 +1,7 @@ import { resolveBaseUrl } from '@pydantic/logfire-api' // simplified interface from CF -interface TraceItem { +export interface TraceItem { // eslint-disable-next-line @typescript-eslint/no-explicit-any logs: { message: any[] }[] } diff --git a/packages/logfire-cf-workers/src/index.ts b/packages/logfire-cf-workers/src/index.ts index 760a3e1..dee5ccb 100644 --- a/packages/logfire-cf-workers/src/index.ts +++ b/packages/logfire-cf-workers/src/index.ts @@ -1,8 +1,31 @@ import { type ReadableSpan, SimpleSpanProcessor, SpanProcessor } from '@opentelemetry/sdk-trace-base' -import * as logfireApi from '@pydantic/logfire-api' -import { resolveBaseUrl, serializeAttributes, ULIDGenerator } from '@pydantic/logfire-api' +import { + configureLogfireApi, + debug, + error, + fatal, + info, + Level, + log, + logfireApiConfig, + LogfireAttributeScrubber, + NoopAttributeScrubber, + notice, + reportError, + resolveBaseUrl, + resolveSendToLogfire, + type ScrubbingOptions, + serializeAttributes, + span, + startSpan, + trace, + ULIDGenerator, + warning, +} from '@pydantic/logfire-api' import { instrument as baseInstrument, TraceConfig } from '@pydantic/otel-cf-workers' +// Import all exports to construct default export +import * as exportTailEventsExports from './exportTailEventsToLogfire' import { LogfireCloudflareConsoleSpanExporter } from './LogfireCloudflareConsoleSpanExporter' import { TailWorkerExporter } from './TailWorkerExporter' export * from './exportTailEventsToLogfire' @@ -27,7 +50,7 @@ export interface InProcessConfigOptions extends ConfigOptionsBase { /** * Options for scrubbing sensitive data. Set to False to disable. */ - scrubbing?: false | logfireApi.ScrubbingOptions + scrubbing?: false | ScrubbingOptions } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -46,7 +69,6 @@ function getInProcessConfig(config: InProcessConfigOptions): (env: Env) => Trace additionalSpanProcessors.push(new SimpleSpanProcessor(new LogfireCloudflareConsoleSpanExporter())) } - console.log({ baseUrl }) return Object.assign({}, config, { additionalSpanProcessors, environment: resolvedEnvironment, @@ -72,7 +94,7 @@ export function getTailConfig(config: TailConfigOptions): (env: Env) => TraceCon export function instrumentInProcess(handler: T, config: InProcessConfigOptions): T { if (config.scrubbing !== undefined) { - logfireApi.configureLogfireApi({ scrubbing: config.scrubbing }) + configureLogfireApi({ scrubbing: config.scrubbing }) } return baseInstrument(handler, getInProcessConfig(config)) as T } @@ -100,3 +122,33 @@ function postProcessAttributes(spans: ReadableSpan[]) { } return spans } + +// Create default export by listing all exports explicitly +export default { + ...exportTailEventsExports, + configureLogfireApi, + debug, + error, + fatal, + getTailConfig, + info, + instrument, + instrumentInProcess, + instrumentTail, + // Re-export all from @pydantic/logfire-api + Level, + log, + logfireApiConfig, + LogfireAttributeScrubber, + NoopAttributeScrubber, + notice, + reportError, + resolveBaseUrl, + resolveSendToLogfire, + serializeAttributes, + span, + startSpan, + trace, + ULIDGenerator, + warning, +} diff --git a/packages/logfire/package.json b/packages/logfire/package.json index 520c515..ae22401 100644 --- a/packages/logfire/package.json +++ b/packages/logfire/package.json @@ -50,7 +50,7 @@ "typecheck": "tsc", "prepack": "cp ../../LICENSE .", "postpack": "rm LICENSE", - "test": "vitest --passWithNoTests" + "test": "vitest run --passWithNoTests" }, "dependencies": { "@pydantic/logfire-api": "*", diff --git a/packages/logfire/src/index.ts b/packages/logfire/src/index.ts index edaf5ff..4d222e5 100644 --- a/packages/logfire/src/index.ts +++ b/packages/logfire/src/index.ts @@ -1,3 +1,57 @@ +import { DiagLogLevel } from '@opentelemetry/api' +import { + configureLogfireApi, + debug, + error, + fatal, + info, + Level, + log, + logfireApiConfig, + LogfireAttributeScrubber, + NoopAttributeScrubber, + notice, + reportError, + resolveBaseUrl, + resolveSendToLogfire, + serializeAttributes, + span, + startSpan, + trace, + ULIDGenerator, + warning, +} from '@pydantic/logfire-api' + +// Import all exports to construct default export +import * as logfireConfigExports from './logfireConfig' + export * from './logfireConfig' export { DiagLogLevel } from '@opentelemetry/api' export * from '@pydantic/logfire-api' + +// Create default export by listing all exports explicitly +export default { + ...logfireConfigExports, + configureLogfireApi, + debug, + DiagLogLevel, + error, + fatal, + info, + // Re-export all from @pydantic/logfire-api + Level, + log, + logfireApiConfig, + LogfireAttributeScrubber, + NoopAttributeScrubber, + notice, + reportError, + resolveBaseUrl, + resolveSendToLogfire, + serializeAttributes, + span, + startSpan, + trace, + ULIDGenerator, + warning, +}