Skip to content

Commit 7378d2b

Browse files
authored
Import otel Cloudflare Workers package (#154)
1 parent c8e87a2 commit 7378d2b

61 files changed

Lines changed: 5438 additions & 164 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@pydantic/logfire-cf-workers": major
3+
"@pydantic/otel-cf-workers": patch
4+
---
5+
6+
Move the Cloudflare Worker OpenTelemetry implementation into the monorepo and publish Cloudflare packages as stable ESM-only packages.
7+
8+
`@pydantic/logfire-cf-workers` now depends on the workspace `@pydantic/otel-cf-workers` package and no longer publishes CommonJS exports or `.d.cts` declarations. `@pydantic/otel-cf-workers` is published from this repository with unified OpenTelemetry catalog dependencies and ESM-only package exports.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This repository is the Pydantic Logfire JavaScript SDK monorepo. It provides Ope
1111
- `packages/logfire-api` publishes `logfire`, the core manual tracing API.
1212
- `packages/logfire-node` publishes `@pydantic/logfire-node`, which adds Node.js SDK setup and automatic instrumentation.
1313
- `packages/logfire-cf-workers` publishes `@pydantic/logfire-cf-workers`, which adapts Logfire to Cloudflare Workers.
14+
- `packages/otel-cf-workers` publishes `@pydantic/otel-cf-workers`, the lower-level Cloudflare Workers OpenTelemetry implementation used by the Logfire wrapper.
1415
- `packages/logfire-browser` publishes `@pydantic/logfire-browser`, which adapts Logfire to browser tracing.
1516
- `packages/tooling-config` contains shared build and lint configuration.
1617
- `examples/` contains runnable examples for Express, Next.js, Deno, Cloudflare Workers, browser usage, and related integrations.

docs/packages/cloudflare.md

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,50 @@ import { instrument as instrumentFunction } from 'logfire'
7373
import { instrument as instrumentWorker } from '@pydantic/logfire-cf-workers'
7474
```
7575

76+
## Durable Objects
77+
78+
Use `instrumentDO()` for Durable Object classes. It uses the same Logfire
79+
configuration path as `instrument()`, including `LOGFIRE_TOKEN`,
80+
`LOGFIRE_ENVIRONMENT`, `LOGFIRE_BASE_URL`, and scrubbing configuration.
81+
82+
```ts
83+
import { instrument, instrumentDO } from '@pydantic/logfire-cf-workers'
84+
85+
class CounterDurableObject implements DurableObject {
86+
constructor(private state: DurableObjectState) {}
87+
88+
async fetch(): Promise<Response> {
89+
const value = (await this.state.storage.get<number>('value')) ?? 0
90+
await this.state.storage.put('value', value + 1)
91+
return Response.json({ value })
92+
}
93+
}
94+
95+
export const Counter = instrumentDO(CounterDurableObject, {
96+
service: {
97+
name: 'counter-do',
98+
namespace: '',
99+
version: '1.0.0',
100+
},
101+
})
102+
103+
export default instrument(
104+
{
105+
async fetch(request, env): Promise<Response> {
106+
const id = env.Counter.idFromName('global')
107+
return env.Counter.get(id).fetch(request)
108+
},
109+
},
110+
{
111+
service: {
112+
name: 'checkout-worker',
113+
namespace: '',
114+
version: '1.0.0',
115+
},
116+
}
117+
)
118+
```
119+
76120
## Tail Workers
77121

78122
The package also supports Cloudflare Tail Worker flows:
@@ -94,12 +138,12 @@ This applies to both Logfire entrypoints:
94138
export.
95139
- `instrumentTail()` configures tail Worker export.
96140

97-
Both entrypoints delegate to the underlying `@pydantic/otel-cf-workers`
98-
`instrument()` helper. That helper proxies the Worker execution context, tracks
99-
promises passed to `ctx.waitUntil()` inside the user handler, then schedules span
100-
export on the original context with `ctx.waitUntil()`. Export waits for a
101-
scheduler tick and the tracked promises before force-flushing the configured
102-
span processors.
141+
Both entrypoints use this repository's Cloudflare Worker OpenTelemetry
142+
instrumentation internally. The instrumentation proxies the Worker execution
143+
context, tracks promises passed to `ctx.waitUntil()` inside the user handler,
144+
then schedules span export on the original context with `ctx.waitUntil()`.
145+
Export waits for a scheduler tick and the tracked promises before force-flushing
146+
the configured span processors.
103147

104148
Because export is tied to each Worker event, there is no long-lived Logfire
105149
runtime to shut down after deployment. Use `ctx.waitUntil()` for any asynchronous

examples/nextjs-bun/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@opentelemetry/sdk-trace-web": "2.8.0",
2020
"@opentelemetry/semantic-conventions": "1.41.1",
2121
"@pydantic/logfire-browser": "0.16.3",
22-
"@vercel/otel": "2.1.2",
22+
"@vercel/otel": "2.1.3",
2323
"next": "16.2.7",
2424
"react": "19.2.7",
2525
"react-dom": "19.2.7"

packages/logfire-cf-workers/README.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,28 @@ import { instrument as instrumentFunction } from 'logfire'
3030
import { instrument as instrumentWorker } from '@pydantic/logfire-cf-workers'
3131
```
3232

33+
Durable Object classes can be wrapped with `instrumentDO()` from this package so
34+
they use the same Logfire token, base URL, environment, and scrubbing
35+
configuration as Worker handlers.
36+
37+
```ts
38+
import { instrumentDO } from '@pydantic/logfire-cf-workers'
39+
40+
class CounterDurableObject implements DurableObject {
41+
async fetch(): Promise<Response> {
42+
return new Response('ok')
43+
}
44+
}
45+
46+
export const Counter = instrumentDO(CounterDurableObject, {
47+
service: {
48+
name: 'counter-do',
49+
namespace: '',
50+
version: '1.0.0',
51+
},
52+
})
53+
```
54+
3355
## Runtime lifecycle
3456

3557
Cloudflare Workers do not have a process-style shutdown hook. Logfire relies on
@@ -42,12 +64,12 @@ This applies to both Logfire entrypoints:
4264
export.
4365
- `instrumentTail()` configures tail Worker export.
4466

45-
Both entrypoints delegate to the underlying `@pydantic/otel-cf-workers`
46-
`instrument()` helper. That helper proxies the Worker execution context, tracks
47-
promises passed to `ctx.waitUntil()` inside the user handler, then schedules span
48-
export on the original context with `ctx.waitUntil()`. Export waits for a
49-
scheduler tick and the tracked promises before force-flushing the configured span
50-
processors.
67+
Both entrypoints use this repository's Cloudflare Worker OpenTelemetry
68+
instrumentation internally. The instrumentation proxies the Worker execution
69+
context, tracks promises passed to `ctx.waitUntil()` inside the user handler,
70+
then schedules span export on the original context with `ctx.waitUntil()`.
71+
Export waits for a scheduler tick and the tracked promises before force-flushing
72+
the configured span processors.
5173

5274
Because export is tied to each Worker event, there is no long-lived Logfire
5375
runtime to shut down after deployment. Use `ctx.waitUntil()` for any asynchronous

packages/logfire-cf-workers/package.json

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,13 @@
2828
],
2929
"version": "0.12.4",
3030
"type": "module",
31-
"main": "./dist/index.cjs",
31+
"main": "./dist/index.js",
3232
"module": "./dist/index.js",
3333
"types": "./dist/index.d.ts",
3434
"exports": {
3535
".": {
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-
}
36+
"types": "./dist/index.d.ts",
37+
"default": "./dist/index.js"
4438
}
4539
},
4640
"scripts": {
@@ -54,7 +48,7 @@
5448
"test": "vp test --passWithNoTests"
5549
},
5650
"dependencies": {
57-
"@pydantic/otel-cf-workers": "^1.0.0-rc.56",
51+
"@pydantic/otel-cf-workers": "workspace:*",
5852
"logfire": "workspace:*"
5953
},
6054
"devDependencies": {

packages/logfire-cf-workers/src/index.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { readdirSync, readFileSync } from 'node:fs'
12
import { describe, expect, it } from 'vite-plus/test'
23
import { instrument as instrumentFunction, startPendingSpan, withSettings, withTags } from 'logfire'
4+
import * as packageRoot from '@pydantic/logfire-cf-workers'
35

46
import logfireCfWorkers, { instrument as instrumentWorker } from './index'
57

@@ -19,4 +21,19 @@ describe('cf-workers default export', () => {
1921
expect(logfireCfWorkers.withSettings).toBe(withSettings)
2022
expect(logfireCfWorkers.withTags).toBe(withTags)
2123
})
24+
25+
it('publishes esm-only package metadata', () => {
26+
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
27+
exports: Record<string, Record<string, string>>
28+
}
29+
30+
expect(packageRoot.instrument).toBeTypeOf('function')
31+
expect(packageRoot.instrumentDO).toBeTypeOf('function')
32+
expect(packageRoot.default.instrument).toBe(packageRoot.instrument)
33+
expect(packageRoot.default.instrumentDO).toBe(packageRoot.instrumentDO)
34+
expect(packageJson.exports['.']).not.toHaveProperty('require')
35+
expect(packageJson.exports['.']?.['default']).toBe('./dist/index.js')
36+
expect(packageJson.exports['.']?.['types']).toBe('./dist/index.d.ts')
37+
expect(readdirSync(new URL('../dist', import.meta.url)).sort()).toEqual(['index.d.ts', 'index.js'])
38+
})
2239
})

packages/logfire-cf-workers/src/index.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base'
22
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
33
import type { TraceConfig } from '@pydantic/otel-cf-workers'
4-
import { instrument as baseInstrument } from '@pydantic/otel-cf-workers'
4+
import { instrument as baseInstrument, instrumentDO as baseInstrumentDO } from '@pydantic/otel-cf-workers'
55
import type { ScrubbingOptions } from 'logfire'
66
import {
77
configureLogfireApi,
@@ -34,7 +34,10 @@ import { LogfireCloudflareConsoleSpanExporter } from './LogfireCloudflareConsole
3434
import { TailWorkerExporter } from './TailWorkerExporter'
3535
export * from './exportTailEventsToLogfire'
3636

37-
type Env = Record<string, string | undefined>
37+
type Env = unknown
38+
type LogfireEnv = Record<string, string | undefined>
39+
type WorkerHandler = ExportedHandler
40+
type DOClass<Env = unknown> = new (state: DurableObjectState, env: Env) => DurableObject
3841

3942
type ConfigOptionsBase = Pick<
4043
TraceConfig,
@@ -60,11 +63,26 @@ export interface InProcessConfigOptions extends ConfigOptionsBase {
6063
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
6164
export interface TailConfigOptions extends ConfigOptionsBase {}
6265

66+
function envString(env: Env, key: string): string | undefined {
67+
if (typeof env !== 'object' || env === null) {
68+
return undefined
69+
}
70+
const value = (env as Record<string, unknown>)[key]
71+
return typeof value === 'string' ? value : undefined
72+
}
73+
74+
function configureScrubbing(config: InProcessConfigOptions): void {
75+
if (config.scrubbing !== undefined) {
76+
configureLogfireApi({ scrubbing: config.scrubbing })
77+
}
78+
}
79+
6380
function getInProcessConfig(config: InProcessConfigOptions): (env: Env) => TraceConfig {
6481
return (env: Env): TraceConfig => {
65-
const { LOGFIRE_ENVIRONMENT: envDeploymentEnvironment, LOGFIRE_TOKEN: token = '' } = env
82+
const token = envString(env, 'LOGFIRE_TOKEN') ?? ''
83+
const envDeploymentEnvironment = envString(env, 'LOGFIRE_ENVIRONMENT')
6684

67-
const baseUrl = resolveBaseUrl(env, config.baseUrl, token)
85+
const baseUrl = resolveBaseUrl(env as LogfireEnv, config.baseUrl, token)
6886
const resolvedEnvironment = config.environment ?? envDeploymentEnvironment
6987

7088
const additionalSpanProcessors = config.additionalSpanProcessors ?? []
@@ -98,14 +116,17 @@ export function getTailConfig(config: TailConfigOptions): (env: Env) => TraceCon
98116
}
99117

100118
export function instrumentInProcess<T>(handler: T, config: InProcessConfigOptions): T {
101-
if (config.scrubbing !== undefined) {
102-
configureLogfireApi({ scrubbing: config.scrubbing })
103-
}
104-
return baseInstrument(handler, getInProcessConfig(config)) as T
119+
configureScrubbing(config)
120+
return baseInstrument(handler as WorkerHandler, getInProcessConfig(config)) as T
121+
}
122+
123+
export function instrumentDO<Env = unknown, T extends DOClass<Env> = DOClass<Env>>(doClass: T, config: InProcessConfigOptions): T {
124+
configureScrubbing(config)
125+
return baseInstrumentDO(doClass, getInProcessConfig(config)) as T
105126
}
106127

107128
export function instrumentTail<T>(handler: T, config: TailConfigOptions): T {
108-
return baseInstrument(handler, getTailConfig(config)) as T
129+
return baseInstrument(handler as WorkerHandler, getTailConfig(config)) as T
109130
}
110131

111132
/**
@@ -140,6 +161,7 @@ const defaultExport: {
140161
getTailConfig: typeof getTailConfig
141162
info: typeof info
142163
instrument: typeof instrument
164+
instrumentDO: typeof instrumentDO
143165
instrumentInProcess: typeof instrumentInProcess
144166
instrumentTail: typeof instrumentTail
145167
log: typeof log
@@ -165,6 +187,7 @@ const defaultExport: {
165187
getTailConfig,
166188
info,
167189
instrument,
190+
instrumentDO,
168191
instrumentInProcess,
169192
instrumentTail,
170193
// Re-export all from logfire
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/// <reference types="vite/client" />
2+
/// <reference types="@cloudflare/workers-types" />

packages/logfire-cf-workers/vite.config.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import { copyFileSync, existsSync } from 'node:fs'
21
import { defineConfig } from 'vite-plus'
32

43
const packageDefines = {
54
PACKAGE_TIMESTAMP: String(Date.now()),
65
PACKAGE_VERSION: JSON.stringify(process.env['npm_package_version'] ?? '0.0.0'),
76
}
87

9-
const copyCjsDeclarations = () => {
10-
if (existsSync('dist/index.d.ts')) {
11-
copyFileSync('dist/index.d.ts', 'dist/index.d.cts')
12-
}
13-
}
14-
158
const config: ReturnType<typeof defineConfig> = defineConfig({
169
define: packageDefines,
1710
pack: {
@@ -23,14 +16,11 @@ const config: ReturnType<typeof defineConfig> = defineConfig({
2316
neverBundle: [/^@opentelemetry/u, /^node:/u, '@pydantic/otel-cf-workers', 'logfire'],
2417
},
2518
entry: 'src/index.ts',
26-
format: ['esm', 'cjs'],
27-
hooks: {
28-
'build:done': copyCjsDeclarations,
29-
},
19+
format: ['esm'],
3020
minify: true,
31-
outExtensions: ({ format }) => ({
32-
dts: format === 'cjs' ? '.d.cts' : '.d.ts',
33-
js: format === 'cjs' ? '.cjs' : '.js',
21+
outExtensions: () => ({
22+
dts: '.d.ts',
23+
js: '.js',
3424
}),
3525
outputOptions: {
3626
exports: 'named',

0 commit comments

Comments
 (0)