├── .changeset
├── README.md
└── config.json
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples
├── cf-producer-worker
│ ├── .editorconfig
│ ├── .gitignore
│ ├── .prettierrc
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── vitest.config.mts
│ ├── worker-configuration.d.ts
│ └── wrangler.jsonc
├── cf-tail-worker
│ ├── .editorconfig
│ ├── .gitignore
│ ├── .prettierrc
│ ├── .vscode
│ │ └── settings.json
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── worker-configuration.d.ts
│ └── wrangler.jsonc
├── cf-worker
│ ├── .editorconfig
│ ├── .gitignore
│ ├── .prettierrc
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ ├── vitest.config.mts
│ ├── worker-configuration.d.ts
│ └── wrangler.jsonc
├── deno-project
│ ├── deno.json
│ ├── main.ts
│ └── start.sh
├── express
│ ├── .env.example
│ ├── app.ts
│ ├── instrumentation.ts
│ ├── package.json
│ └── tsconfig.json
├── nextjs-client-side-instrumentation
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ │ ├── api
│ │ │ └── hello
│ │ │ │ └── route.ts
│ │ ├── components
│ │ │ ├── ClientInstrumentationProvider.tsx
│ │ │ └── HelloButton.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── eslint.config.mjs
│ ├── instrumentation.ts
│ ├── middleware.ts
│ ├── next.config.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── file.svg
│ │ ├── globe.svg
│ │ ├── next.svg
│ │ ├── vercel.svg
│ │ └── window.svg
│ └── tsconfig.json
├── nextjs
│ ├── .codesandbox
│ │ └── tasks.json
│ ├── .gitignore
│ ├── app
│ │ ├── favicon.ico
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── instrumentation.ts
│ ├── middleware.ts
│ ├── next.config.ts
│ ├── package.json
│ └── tsconfig.json
└── node
│ ├── .env
│ ├── .env.example
│ ├── index.ts
│ ├── package.json
│ └── tsconfig.json
├── package-lock.json
├── package.json
├── packages
├── logfire-api
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── eslint.config.mjs
│ ├── package.json
│ ├── prettier.config.mjs
│ ├── src
│ │ ├── AttributeScrubber.ts
│ │ ├── constants.ts
│ │ ├── formatter.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── logfireApiConfig.test.ts
│ │ ├── logfireApiConfig.ts
│ │ ├── serializeAttributes.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── logfire-cf-workers
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── eslint.config.mjs
│ ├── package.json
│ ├── prettier.config.mjs
│ ├── src
│ │ ├── OtlpTransformerTypes.ts
│ │ ├── TailWorkerExporter.ts
│ │ ├── ULIDGenerator.ts
│ │ ├── exportTailEventsToLogfire.ts
│ │ ├── index.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── logfire
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── eslint.config.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── prettier.config.mjs
│ ├── src
│ │ ├── ULIDGenerator.ts
│ │ ├── VoidMetricExporter.ts
│ │ ├── VoidTraceExporter.ts
│ │ ├── index.ts
│ │ ├── logfireConfig.ts
│ │ ├── metricExporter.ts
│ │ ├── sdk.ts
│ │ ├── traceExporter.ts
│ │ ├── utils.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
└── tooling-config
│ ├── eslint-config.d.mts
│ ├── eslint-config.mjs
│ ├── package.json
│ ├── prettier-config.d.mts
│ ├── prettier-config.mjs
│ ├── tsconfig.base.json
│ ├── vite-config.d.mts
│ └── vite-config.mjs
└── turbo.json
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | env:
7 | CI: true
8 | steps:
9 | - name: Begin CI...
10 | uses: actions/checkout@v4
11 |
12 | - name: Use Node 22
13 | uses: actions/setup-node@v4
14 | with:
15 | node-version: 22.x
16 |
17 | - name: Install dependencies
18 | run: |
19 | npm install
20 | npm install --workspaces
21 |
22 | - name: run CI checks
23 | run: |
24 | npm run ci
25 |
26 | - name: Release
27 | id: changesets
28 | if: github.ref == 'refs/heads/main'
29 | uses: changesets/action@v1
30 | with:
31 | publish: npm run release
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | packages/*/*.tgz
3 | .turbo
4 | .env
5 | CLAUDE.md
6 | scratch/
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — Uncomplicated Observability — JavaScript SDK
2 |
3 | From the team behind [Pydantic](https://pydantic.dev/), **Logfire** is an
4 | observability platform built on the same belief as our open source library —
5 | that the most powerful tools can be easy to use.
6 |
7 | What sets Logfire apart:
8 |
9 | - **Simple and Powerful:** Logfire's dashboard is simple relative to the power
10 | it provides, ensuring your entire engineering team will actually use it.
11 | - **SQL:** Query your data using standard SQL — all the control and (for many)
12 | nothing new to learn. Using SQL also means you can query your data with
13 | existing BI tools and database querying libraries.
14 | - **OpenTelemetry:** Logfire is an opinionated wrapper around OpenTelemetry,
15 | allowing you to leverage existing tooling, infrastructure, and instrumentation
16 | for many common packages, and enabling support for virtually any language. We
17 | offer full support for all OpenTelemetry signals (traces, metrics, and logs).
18 |
19 | **Feel free to report issues and ask any questions about Logfire in this
20 | repository!**
21 |
22 | This repository contains the JavaScript SDK for `logfire` and its documentation;
23 | the server application for recording and displaying data is closed source.
24 |
25 |
26 |
27 | ## Usage
28 |
29 | Depending on your environment, you can integrate Logfire in several ways. Follow
30 | the specific instructions below:
31 |
32 | ### Basic Node.js script
33 |
34 | Using Logfire from your Node.js script is as simple as
35 | [getting a write token](https://logfire.pydantic.dev/docs/how-to-guides/create-write-tokens/),
36 | installing the package, calling configure, and using the provided API. Let's
37 | create an empty project:
38 |
39 | ```sh
40 | mkdir test-logfire-js
41 | cd test-logfire-js
42 | npm init -y es6 # creates package.json with `type: module`
43 | npm install logfire
44 | ```
45 |
46 | Then, create the following `hello.js` script in the directory:
47 |
48 | ```js
49 | import * as logfire from "logfire";
50 |
51 | logfire.configure({
52 | token: "test-e2e-write-token",
53 | advanced: {
54 | baseUrl: "http://localhost:8000",
55 | },
56 | serviceName: "example-node-script",
57 | serviceVersion: "1.0.0",
58 | });
59 |
60 | logfire.info("Hello from Node.js", {
61 | "attribute-key": "attribute-value",
62 | }, {
63 | tags: ["example", "example2"],
64 | });
65 | ```
66 |
67 | Run the script with `node hello.js`, and you should see the span being logged in
68 | the live view of your Logfire project.
69 |
70 | ### Cloudflare Workers
71 |
72 | First, install the `@pydantic/logfire-cf-workers @pydantic/logfire-api` NPM
73 | packages:
74 |
75 | ```sh
76 | npm install @pydantic/logfire-cf-workers @pydantic/logfire-api
77 | ```
78 |
79 | Next, add `compatibility_flags = [ "nodejs_compat" ]` to your wrangler.toml or
80 | `"compatibility_flags": ["nodejs_compat"]` if you're using `wrangler.jsonc`.
81 |
82 | Add your
83 | [Logfire write token](https://logfire.pydantic.dev/docs/how-to-guides/create-write-tokens/)
84 | to your `.dev.vars` file:
85 |
86 | ```sh
87 | LOGFIRE_TOKEN=your-write-token
88 | ```
89 |
90 | For production deployment, check the
91 | [Cloudflare documentation for details on managing and deploying secrets](https://developers.cloudflare.com/workers/configuration/secrets/).
92 |
93 | One way to do this is through the `npx wrangler` command:
94 |
95 | ```sh
96 | npx wrangler secret put LOGFIRE_TOKEN
97 | ```
98 |
99 | Next, add the necessary instrumentation around your handler. The `tracerConfig`
100 | function will extract your write token from the `env` object and provide the
101 | necessary configuration for the instrumentation:
102 |
103 | ```ts
104 | import * as logfire from "@pydantic/logfire-api";
105 | import { instrument } from "@pydantic/logfire-cf-workers";
106 |
107 | const handler = {
108 | async fetch(): Promise {
109 | logfire.info("info span from inside the worker body");
110 | return new Response("hello world!");
111 | },
112 | } satisfies ExportedHandler;
113 |
114 | export default instrument(handler, {
115 | serviceName: "cloudflare-worker",
116 | serviceNamespace: "",
117 | serviceVersion: "1.0.0",
118 | });
119 | ```
120 |
121 | A working example can be found in the `examples/cloudflare-worker` directory.
122 |
123 | ### Next.js/Vercel
124 |
125 | Vercel provides a comprehensive OpenTelemetry integration through the
126 | `@vercel/otel` package. After following
127 | [their integration instructions](https://vercel.com/docs/otel), add the
128 | following environment variables to your project:
129 |
130 | ```sh
131 | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://logfire-api.pydantic.dev/v1/traces
132 | OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://logfire-api.pydantic.dev/v1/metrics
133 | OTEL_EXPORTER_OTLP_HEADERS='Authorization=your-write-token'
134 | ```
135 |
136 | This will point the instrumentation to Logfire.
137 |
138 | > [!NOTE]
139 | > Vercel production deployments have a caching mechanism that might prevent
140 | > changes from taking effect immediately or spans from being reported. If you
141 | > are not seeing spans in Logfire, you can
142 | > [clear the data cache for your project](https://vercel.com/docs/data-cache/manage-data-cache).
143 |
144 | Optionally, you can use the Logfire API package for creating manual spans.
145 | Install the `@pydantic/logfire-api` NPM package and call the respective methods
146 | from your server-side code:
147 |
148 | ```tsx
149 | import * as logfire from "@pydantic/logfire-api";
150 |
151 | export default async function Home() {
152 | return logfire.span("A warning span", {}, {
153 | level: logfire.Level.Warning,
154 | }, async (span) => {
155 | logfire.info("Nested info span");
156 | // ending the span is necessary to ensure it is reported
157 | span.end();
158 | return Hello
;
159 | });
160 | }
161 | ```
162 |
163 | A working example can be found in the `examples/nextjs` directory.
164 |
165 | #### Next.js client-side instrumentation
166 |
167 | The `@vercel/otel` package does not support client-side instrumentation, so few additional steps are necessary to send spans and/or instrument the client-side.
168 | For a working example, refer to the `examples/nextjs-client-side-instrumentation` directory, which instruments the client-side `fetch` calls.
169 |
170 | ### Express, generic Node instrumentation
171 |
172 | For this example, we will instrument a simple Express app:
173 |
174 | ```ts
175 | /*app.ts*/
176 | import express, type { Express } from 'express';
177 |
178 | const PORT: number = parseInt(process.env.PORT || '8080');
179 | const app: Express = express();
180 |
181 | function getRandomNumber(min: number, max: number) {
182 | return Math.floor(Math.random() * (max - min + 1) + min);
183 | }
184 |
185 | app.get('/rolldice', (req, res) => {
186 | res.send(getRandomNumber(1, 6).toString());
187 | });
188 |
189 | app.listen(PORT, () => {
190 | console.log(`Listening for requests on http://localhost:${PORT}`);
191 | });
192 | ```
193 |
194 | Next, install the `logfire` and `dotenv` NPM packages to keep your Logfire write
195 | token in a `.env` file:
196 |
197 | ```sh
198 | npm install logfire dotenv
199 | ```
200 |
201 | Add your token to the `.env` file:
202 |
203 | ```sh
204 | LOGFIRE_TOKEN=your-write-token
205 | ```
206 |
207 | Then, create an `instrumentation.ts` file to set up the instrumentation. The
208 | `logfire` package includes a `configure` function that simplifies the setup:
209 |
210 | ```ts
211 | // instrumentation.ts
212 | import * as logfire from "logfire";
213 | import "dotenv/config";
214 |
215 | logfire.configure();
216 | ```
217 |
218 | The `logfire.configure` call should happen before the actual express module
219 | imports, so your NPM start script should look like this (`package.json`):
220 |
221 | ```json
222 | "scripts": {
223 | "start": "npx ts-node --require ./instrumentation.ts app.ts"
224 | },
225 | ```
226 |
227 | ## Deno
228 |
229 | Deno has
230 | [built-in support for OpenTelemetry](https://docs.deno.com/runtime/fundamentals/open_telemetry/).
231 | The examples directory includes a `Hello world` example that configures Deno
232 | OTel export to Logfire through environment variables.
233 |
234 | Optionally, you can use the Logfire API package for creating manual spans.
235 | Install the `@pydantic/logfire-api` NPM package and call the respective methods
236 | from your code.
237 |
238 | ### Configuring the instrumentation
239 |
240 | The `logfire.configure` function accepts a set of configuration options that
241 | control the behavior of the instrumentation. Alternatively, you can
242 | [use environment variables](https://logfire.pydantic.dev/docs/reference/configuration/#programmatically-via-configure)
243 | to configure the instrumentation.
244 |
245 | ## Trace API
246 |
247 | The `@pydantic/logfire-api` exports several convenience wrappers around the
248 | OpenTelemetry span creation API. The `logfire` package re-exports these.
249 |
250 | The following methods create spans with their respective log levels (ordered by
251 | severity):
252 |
253 | - `logfire.trace`
254 | - `logfire.debug`
255 | - `logfire.info`
256 | - `logfire.notice`
257 | - `logfire.warn`
258 | - `logfire.error`
259 | - `logfire.fatal`
260 |
261 | Each method accepts a message, attributes, and optionally, options that let you
262 | specify the span tags. The attribute values must be serializable to JSON.
263 |
264 | ```ts
265 | function info(
266 | message: string,
267 | attributes?: Record,
268 | options?: LogOptions,
269 | ): void;
270 | ```
271 |
272 | ## Contributing
273 |
274 | See [CONTRIBUTING.md](CONTRIBUTING.md) for development instructions.
275 |
276 | ## License
277 |
278 | MIT
279 |
--------------------------------------------------------------------------------
/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-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-producer-worker/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/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 | "@pydantic/logfire-api": "*",
21 | "@pydantic/logfire-cf-workers": "*"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/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 '@pydantic/logfire-api';
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/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-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-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/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/.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/.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/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/examples/cf-tail-worker/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "wrangler.json": "jsonc"
4 | }
5 | }
--------------------------------------------------------------------------------
/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 | "test": "vitest",
10 | "cf-typegen": "wrangler types"
11 | },
12 | "devDependencies": {
13 | "@cloudflare/vitest-pool-workers": "^0.8.19",
14 | "@cloudflare/workers-types": "^4.20250425.0",
15 | "typescript": "^5.5.2",
16 | "vitest": "~3.0.7",
17 | "wrangler": "^4.13.2"
18 | },
19 | "dependencies": {
20 | "@pydantic/logfire-cf-workers": "*"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/.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/.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-worker/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/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/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 | "@pydantic/logfire-api": "*",
20 | "@pydantic/logfire-cf-workers": "*"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/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 '@pydantic/logfire-api';
14 | import { instrument } from '@pydantic/logfire-cf-workers';
15 |
16 | const handler = {
17 | async fetch(): Promise {
18 | logfire.info('info span from inside the worker body');
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 | });
30 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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/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/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/deno-project/main.ts:
--------------------------------------------------------------------------------
1 | Deno.serve({ port: 4242 }, (_req) => {
2 | return new Response("Hello, World!");
3 | });
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/express/app.ts:
--------------------------------------------------------------------------------
1 | import type { Express, Request, Response } from "express";
2 | import express from "express";
3 | import * as logfire from "logfire";
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 |
--------------------------------------------------------------------------------
/examples/express/instrumentation.ts:
--------------------------------------------------------------------------------
1 | import * as logfire from 'logfire'
2 | import 'dotenv/config'
3 |
4 | logfire.configure({
5 | diagLogLevel: logfire.DiagLogLevel.ERROR,
6 | })
7 |
--------------------------------------------------------------------------------
/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 | "logfire": "*",
30 | "tsx": "^4.19.0",
31 | "typescript": "^5.5.4"
32 | },
33 | "devDependencies": {
34 | "@tsconfig/node20": "^20.1.4"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/express/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node20/tsconfig.json",
3 | "compilerOptions": {
4 | "moduleDetection": "force"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/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/README.md:
--------------------------------------------------------------------------------
1 | # Example for a Next.js client/server distributed OTel instrumentation with Logfire
2 |
3 |
4 | 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 Logfire OTel SDK for both the client and server sides.
5 |
6 | ## Highlights
7 |
8 | - The `ClientInstrumentationProvider` is a client-only component that instruments the browser fetch.
9 | - To avoid exposing the write token, the middleware.ts proxies the logfire `/v1/traces` request.
10 | - The instrumentation.ts file is the standard `@vercel/otel` setup.
11 | - The `.env` should look like this:
12 |
13 | ```sh
14 | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://logfire-api.pydantic.dev/v1/traces
15 | OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://logfire-api.pydantic.dev/v1/metrics
16 | OTEL_EXPORTER_OTLP_HEADERS='Authorization=your-token'
17 | LOGFIRE_TOKEN='your-token'
18 | ```
19 |
20 | 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).
21 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/api/hello/route.ts:
--------------------------------------------------------------------------------
1 | import * as logfire from "@pydantic/logfire-api";
2 |
3 | export async function GET() {
4 | logfire.info("server span");
5 | return Response.json({ message: "Hello World!" });
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/app/components/ClientInstrumentationProvider.tsx:
--------------------------------------------------------------------------------
1 | import { ZoneContextManager } from "@opentelemetry/context-zone";
2 | import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
3 | import { registerInstrumentations } from "@opentelemetry/instrumentation";
4 | import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
5 | import { Resource } from "@opentelemetry/resources";
6 | import {
7 | RandomIdGenerator,
8 | SimpleSpanProcessor,
9 | WebTracerProvider,
10 | } from "@opentelemetry/sdk-trace-web";
11 | import {
12 | ATTR_SERVICE_NAME,
13 | ATTR_SERVICE_VERSION,
14 | } from "@opentelemetry/semantic-conventions";
15 | import { ReactNode, useEffect } from "react";
16 |
17 | // JS port of https://github.com/pydantic/logfire/blob/main/logfire/_internal/ulid.py without the parameters
18 | function ulid(): bigint {
19 | // Timestamp: first 6 bytes of the ULID (48 bits)
20 | // Note that it's not important that this timestamp is super precise or unique.
21 | // It just needs to be roughly monotonically increasing so that the ULID is sortable, at least for our purposes.
22 | let result = BigInt(Date.now());
23 |
24 | // Randomness: next 10 bytes of the ULID (80 bits)
25 | const randomness = crypto.getRandomValues(new Uint8Array(10));
26 | for (const segment of randomness) {
27 | result <<= BigInt(8);
28 | result |= BigInt(segment);
29 | }
30 |
31 | return result;
32 | }
33 |
34 | class ULIDGenerator extends RandomIdGenerator {
35 | override generateTraceId = () => {
36 | return ulid().toString(16).padStart(32, "0");
37 | };
38 | }
39 |
40 | export default function ClientInstrumentationProvider(
41 | { children }: { children: ReactNode },
42 | ) {
43 | useEffect(() => {
44 | const url = new URL(window.location.href);
45 | url.pathname = "/client-traces";
46 | const resource = new Resource({
47 | [ATTR_SERVICE_NAME]: "logfire-frontend",
48 | [ATTR_SERVICE_VERSION]: "0.0.1",
49 | });
50 |
51 | const provider = new WebTracerProvider({
52 | resource,
53 | idGenerator: new ULIDGenerator(),
54 | spanProcessors: [
55 | new SimpleSpanProcessor(
56 | new OTLPTraceExporter({ url: url.toString() }),
57 | ),
58 | ],
59 | });
60 |
61 | provider.register({
62 | contextManager: new ZoneContextManager(),
63 | });
64 |
65 | registerInstrumentations({
66 | instrumentations: [new FetchInstrumentation()],
67 | });
68 | }, []);
69 | return children;
70 | }
71 |
--------------------------------------------------------------------------------
/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/nextjs-client-side-instrumentation/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pydantic/logfire-js/ed0a79eec5cca579e6823bf6e8be05d3b74bdcfa/examples/nextjs-client-side-instrumentation/app/favicon.ico
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/instrumentation.ts:
--------------------------------------------------------------------------------
1 | import { registerOTel } from "@vercel/otel";
2 |
3 | export function register() {
4 | registerOTel({ serviceName: "sample-project" });
5 | }
6 | // NOTE: You can replace `your-project-name` with the actual name of your project
7 |
--------------------------------------------------------------------------------
/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 | console.log(process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT);
7 | if (url.pathname === "/client-traces") {
8 | const requestHeaders = new Headers(request.headers);
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 | headers: requestHeaders,
15 | },
16 | );
17 | }
18 | }
19 |
20 | export const config = {
21 | matcher: "/client-traces/:path*",
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 | };
6 |
7 | export default nextConfig;
8 |
--------------------------------------------------------------------------------
/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",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@opentelemetry/api": "^1.9.0",
13 | "@opentelemetry/context-zone": "^1.30.1",
14 | "@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
15 | "@opentelemetry/instrumentation": "^0.57.2",
16 | "@opentelemetry/instrumentation-fetch": "^0.57.2",
17 | "@opentelemetry/resources": "^1.19.0",
18 | "@opentelemetry/sdk-trace-web": "^1.30.1",
19 | "@opentelemetry/semantic-conventions": "^1.32.0",
20 | "@pydantic/logfire-api": "^0.4.0",
21 | "@vercel/otel": "^1.11.0",
22 | "next": "15.3.0",
23 | "react": "^19.0.0",
24 | "react-dom": "^19.0.0"
25 | },
26 | "devDependencies": {
27 | "@eslint/eslintrc": "^3",
28 | "@types/node": "^20",
29 | "@types/react": "^19",
30 | "@types/react-dom": "^19",
31 | "eslint": "^9",
32 | "eslint-config-next": "15.3.0",
33 | "typescript": "^5"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/nextjs-client-side-instrumentation/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/.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/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pydantic/logfire-js/ed0a79eec5cca579e6823bf6e8be05d3b74bdcfa/examples/nextjs/app/favicon.ico
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/nextjs/app/page.tsx:
--------------------------------------------------------------------------------
1 | import * as logfire from '@pydantic/logfire-api'
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/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/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 |
--------------------------------------------------------------------------------
/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/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 | "@pydantic/logfire-api": "*",
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/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 |
--------------------------------------------------------------------------------
/examples/node/.env:
--------------------------------------------------------------------------------
1 | LOGFIRE_BASE_URL=http://localhost:8000
2 | LOGFIRE_TOKEN=test-e2e-write-token
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/node/index.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config'
2 | import * as logfire from 'logfire'
3 |
4 | logfire.configure({
5 | serviceName: 'example-node-script',
6 | serviceVersion: '1.0.0',
7 | environment: 'staging',
8 | token: 'pylf_v1_eu_fcksvB6FNdWKZ3xGbrG8g8GXHFqPfFXgtRgnZdvV6PCj',
9 | diagLogLevel: logfire.DiagLogLevel.DEBUG,
10 | codeSource: {
11 | repository: 'https://github.com/pydantic/pydantic',
12 | revision: 'master',
13 | },
14 | })
15 |
16 |
17 | logfire.span('Hello from Node.js, {next_player}', {
18 | 'attribute-key': 'attribute-value',
19 | next_player: '0',
20 | arr: [1, 2, 3],
21 | something: {
22 | value: [1, 2, 3],
23 | key: 'value'
24 | }
25 | }, {
26 | tags: ['example', 'example2']
27 | }, (span) => {
28 | span.end()
29 | })
30 |
--------------------------------------------------------------------------------
/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 | "logfire": "*"
20 | },
21 | "devDependencies": {
22 | "@opentelemetry/semantic-conventions": "^1.30.0",
23 | "@tsconfig/node22": "^22.0.0",
24 | "typescript": "^5.8.2"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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"
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 |
--------------------------------------------------------------------------------
/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-api/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @pydantic/logfire-api
2 |
3 | ## 0.4.1
4 |
5 | ### Patch Changes
6 |
7 | - cd2ac40: Fix attribute serialization
8 |
9 | ## 0.4.0
10 |
11 | ### Minor Changes
12 |
13 | - dc0a537: Support for EU tokens. Support span message formatting.
14 |
15 | ## 0.3.0
16 |
17 | ### Minor Changes
18 |
19 | - 6fa1410: API updates, fixes for span kind
20 |
21 | ## 0.2.1
22 |
23 | ### Patch Changes
24 |
25 | - 838ba5d: Fix packages publish settings.
26 |
27 | ## 0.2.0
28 |
29 | ### Minor Changes
30 |
31 | - 0f0ce8f: Initial release.
32 |
--------------------------------------------------------------------------------
/packages/logfire-api/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — Uncomplicated Observability — JavaScript SDK
2 |
3 | From the team behind [Pydantic](https://pydantic.dev/), **Logfire** is an observability platform built on the same belief as our
4 | open source library — that the most powerful tools can be easy to use.
5 |
6 | Check the [Github Repository README](https://github.com/pydantic/logfire-js) for more information on how to use the SDK.
7 |
--------------------------------------------------------------------------------
/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-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pydantic/logfire-api",
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 | "sideEffects": false,
11 | "homepage": "https://pydantic.dev/logfire",
12 | "license": "MIT",
13 | "publishConfig": {
14 | "access": "public"
15 | },
16 | "keywords": [
17 | "logfire",
18 | "observability",
19 | "opentelemetry",
20 | "tracing",
21 | "profiling",
22 | "stats",
23 | "monitoring"
24 | ],
25 | "version": "0.4.1",
26 | "type": "module",
27 | "main": "./dist/index.cjs",
28 | "module": "./dist/index.js",
29 | "types": "./dist/index.d.ts",
30 | "exports": {
31 | ".": {
32 | "import": {
33 | "types": "./dist/index.d.ts",
34 | "default": "./dist/index.js"
35 | },
36 | "require": {
37 | "types": "./dist/index.d.cts",
38 | "default": "./dist/index.cjs"
39 | }
40 | }
41 | },
42 | "scripts": {
43 | "dev": "vite build",
44 | "build": "vite build",
45 | "lint": "eslint",
46 | "preview": "vite preview",
47 | "typecheck": "tsc",
48 | "prepack": "cp ../../LICENSE .",
49 | "postpack": "rm LICENSE",
50 | "test": "vitest"
51 | },
52 | "devDependencies": {
53 | "@opentelemetry/api": "^1.9.0",
54 | "@pydantic/logfire-tooling-config": "*",
55 | "eslint": "^9.22.0",
56 | "prettier": "3.5.3",
57 | "typescript": "^5.8.2",
58 | "vite": "^6.2.0",
59 | "vite-plugin-dts": "^4.5.3",
60 | "vitest": "^3.1.1"
61 | },
62 | "peerDependencies": {
63 | "@opentelemetry/api": "^1.9.0"
64 | },
65 | "files": [
66 | "dist",
67 | "LICENSE"
68 | ]
69 | }
70 |
--------------------------------------------------------------------------------
/packages/logfire-api/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/prettier-config'
2 |
3 | export default baseConfig
4 |
--------------------------------------------------------------------------------
/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 | matchedSubstring: 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({ 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-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 |
11 | /** Key for storing scrubbed attributes information */
12 | export const ATTRIBUTES_SCRUBBED_KEY = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.scrubbed`
13 | export const DEFAULT_OTEL_SCOPE = 'logfire'
14 | export const JSON_SCHEMA_KEY = 'logfire.json_schema'
15 | export const JSON_NULL_FIELDS_KEY = 'logfire.null_args'
16 |
--------------------------------------------------------------------------------
/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('.')
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 | if (result.length > 0 && result[result.length - 1].type === 'lit') {
174 | result[result.length - 1].value += fieldName
175 | } else {
176 | result.push({ type: 'lit', value: fieldName })
177 | }
178 | actualFieldName = fieldName.slice(0, -1)
179 | }
180 |
181 | // Get the object referenced by the field name
182 | let obj
183 | try {
184 | ;[obj] = this.getField(actualFieldName, record)
185 | } catch (err) {
186 | if (err instanceof KnownFormattingError) {
187 | throw err
188 | }
189 | throw new KnownFormattingError(`Error getting field ${actualFieldName}: ${String(err)}`)
190 | }
191 |
192 | // Format the field value
193 | let formattedValue
194 | try {
195 | formattedValue = this.formatField(obj, formatSpec ?? '')
196 | } catch (err) {
197 | throw new KnownFormattingError(`Error formatting field ${actualFieldName}: ${String(err)}`)
198 | }
199 |
200 | // Clean and scrub the value
201 | const [cleanValue, valueScrubbed] = this.cleanValue(actualFieldName, formattedValue, scrubber)
202 | scrubbed.push(...valueScrubbed)
203 |
204 | const argChunk: ArgChunk = { type: 'arg', value: cleanValue }
205 | if (formatSpec) {
206 | argChunk.spec = formatSpec
207 | }
208 | result.push(argChunk)
209 | }
210 | }
211 |
212 | const extraAttrs = scrubbed.length > 0 ? { [ATTRIBUTES_SCRUBBED_KEY]: scrubbed } : {}
213 | return [result, extraAttrs]
214 | }
215 | }
216 |
217 | // Create singleton instance
218 | export const chunksFormatter = new ChunksFormatter()
219 |
220 | /**
221 | * Format a string using a Python-like template syntax
222 | */
223 | export function logfireFormat(formatString: string, record: Record, scrubber: BaseScrubber): string {
224 | return logfireFormatWithExtras(formatString, record, scrubber)[0]
225 | }
226 |
227 | /**
228 | * Format a string with additional information about attributes and templates
229 | */
230 | export function logfireFormatWithExtras(
231 | formatString: string,
232 | record: Record,
233 | scrubber: BaseScrubber
234 | ): [string, Record, string] {
235 | try {
236 | const [chunks, extraAttrs, newTemplate] = chunksFormatter.chunks(formatString, record, scrubber)
237 |
238 | return [chunks.map((chunk) => chunk.value).join(''), extraAttrs, newTemplate]
239 | } catch (err) {
240 | if (err instanceof KnownFormattingError) {
241 | console.warn(`Formatting error: ${err.message}`)
242 | } else {
243 | console.error('Unexpected error during formatting:', err)
244 | }
245 |
246 | // Formatting failed, use the original format string as the message
247 | return [formatString, {}, formatString]
248 | }
249 | }
250 |
251 | /**
252 | * Truncates a string if it exceeds the specified maximum length.
253 | *
254 | * @param str The string to truncate
255 | * @param maxLength The maximum allowed length
256 | * @returns The truncated string
257 | */
258 | export function truncateString(str: string, maxLength: number): string {
259 | if (str.length <= maxLength) {
260 | return str
261 | }
262 |
263 | // Truncate and add ellipsis
264 | return str.substring(0, maxLength - 3) + '...'
265 | }
266 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { trace } from '@opentelemetry/api'
2 | import { expect, test, vi } from 'vitest'
3 |
4 | import { ATTRIBUTES_LEVEL_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY } from './constants'
5 | import { info } from './index'
6 |
7 | vi.mock('@opentelemetry/api', () => {
8 | const spanMock = {
9 | end: vi.fn(),
10 | setAttribute: vi.fn(),
11 | setStatus: vi.fn(),
12 | }
13 |
14 | const tracerMock = {
15 | startSpan: vi.fn(() => spanMock),
16 | }
17 |
18 | return {
19 | context: {
20 | active: vi.fn(),
21 | },
22 | trace: {
23 | getTracer: vi.fn(() => tracerMock),
24 | },
25 | }
26 | })
27 |
28 | test('formats the message with the passed attributes', () => {
29 | info('aha {i}', { i: 1 })
30 | const tracer = trace.getTracer('logfire')
31 |
32 | // eslint-disable-next-line @typescript-eslint/unbound-method
33 | expect(tracer.startSpan).toBeCalledWith(
34 | 'aha 1',
35 | {
36 | attributes: {
37 | [ATTRIBUTES_LEVEL_KEY]: 9,
38 | [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: 'aha {i}',
39 | [ATTRIBUTES_SPAN_TYPE_KEY]: 'log',
40 | [ATTRIBUTES_TAGS_KEY]: [],
41 | i: 1,
42 | },
43 | },
44 | undefined
45 | )
46 | })
47 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable perfectionist/sort-objects */
2 | import { Span, SpanStatusCode } from '@opentelemetry/api'
3 | import { ATTR_EXCEPTION_MESSAGE, ATTR_EXCEPTION_STACKTRACE } from '@opentelemetry/semantic-conventions'
4 |
5 | import { ScrubCallback } from './AttributeScrubber'
6 | import { ATTRIBUTES_LEVEL_KEY, ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY } from './constants'
7 | import { logfireFormatWithExtras } from './formatter'
8 | import { logfireApiConfig, serializeAttributes } from './logfireApiConfig'
9 |
10 | export * from './AttributeScrubber'
11 | export { configureLogfireApi, logfireApiConfig, resolveBaseUrl, resolveSendToLogfire } from './logfireApiConfig'
12 | export { serializeAttributes } from './serializeAttributes'
13 |
14 | export interface SrubbingOptions {
15 | callback?: ScrubCallback
16 | extraPatterns?: string[]
17 | }
18 |
19 | export interface LogfireApiConfigOptions {
20 | otelScope?: string
21 | /**
22 | * Options for scrubbing sensitive data. Set to False to disable.
23 | */
24 | scrubbing?: false | SrubbingOptions
25 | }
26 |
27 | export const Level = {
28 | Trace: 1 as const,
29 | Debug: 5 as const,
30 | Info: 9 as const,
31 | Notice: 10 as const,
32 | Warning: 13 as const,
33 | Error: 17 as const,
34 | Fatal: 21 as const,
35 | }
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 function startSpan(
46 | msgTemplate: string,
47 | attributes: Record = {},
48 | { log, tags = [], level = Level.Info }: LogOptions = {}
49 | ): Span {
50 | // TODO: should we also send the extra attributes (2nd arg)?
51 | const [formattedMessage, , newTemplate] = logfireFormatWithExtras(msgTemplate, attributes, logfireApiConfig.scrubber)
52 | const span = logfireApiConfig.tracer.startSpan(
53 | formattedMessage,
54 | {
55 | attributes: {
56 | ...serializeAttributes(attributes),
57 | [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: newTemplate,
58 | [ATTRIBUTES_LEVEL_KEY]: level,
59 | [ATTRIBUTES_TAGS_KEY]: Array.from(new Set(tags).values()),
60 | [ATTRIBUTES_SPAN_TYPE_KEY]: log ? 'log' : 'span',
61 | },
62 | },
63 | logfireApiConfig.context
64 | )
65 |
66 | return span
67 | }
68 |
69 | export function span unknown>(
70 | msgTemplate: string,
71 | attributes: Record = {},
72 | { tags = [], level = Level.Info }: LogOptions = {},
73 | callback: F
74 | ) {
75 | const [formattedMessage, , newTemplate] = logfireFormatWithExtras(msgTemplate, attributes, logfireApiConfig.scrubber)
76 | return logfireApiConfig.tracer.startActiveSpan(
77 | formattedMessage,
78 | {
79 | attributes: {
80 | ...serializeAttributes(attributes),
81 | [ATTRIBUTES_MESSAGE_TEMPLATE_KEY]: newTemplate,
82 | [ATTRIBUTES_LEVEL_KEY]: level,
83 | [ATTRIBUTES_TAGS_KEY]: Array.from(new Set(tags).values()),
84 | },
85 | },
86 | callback
87 | )
88 | }
89 |
90 | export function log(message: string, attributes: Record = {}, options: LogOptions = {}) {
91 | startSpan(message, attributes, { ...options, log: true }).end()
92 | }
93 |
94 | export function debug(message: string, attributes: Record = {}, options: LogOptions = {}) {
95 | log(message, attributes, { ...options, level: Level.Debug })
96 | }
97 |
98 | export function info(message: string, attributes: Record = {}, options: LogOptions = {}) {
99 | log(message, attributes, { ...options, level: Level.Info })
100 | }
101 |
102 | export function trace(message: string, attributes: Record = {}, options: LogOptions = {}) {
103 | log(message, attributes, { ...options, level: Level.Trace })
104 | }
105 |
106 | export function error(message: string, attributes: Record = {}, options: LogOptions = {}) {
107 | log(message, attributes, { ...options, level: Level.Error })
108 | }
109 |
110 | export function fatal(message: string, attributes: Record = {}, options: LogOptions = {}) {
111 | log(message, attributes, { ...options, level: Level.Fatal })
112 | }
113 |
114 | export function notice(message: string, attributes: Record = {}, options: LogOptions = {}) {
115 | log(message, attributes, { ...options, level: Level.Notice })
116 | }
117 |
118 | export function warning(message: string, attributes: Record = {}, options: LogOptions = {}) {
119 | log(message, attributes, { ...options, level: Level.Warning })
120 | }
121 |
122 | export function reportError(message: string, error: Error, extraAttributes: Record = {}) {
123 | const span = startSpan(message, {
124 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
125 | [ATTR_EXCEPTION_MESSAGE]: error.message ?? 'error',
126 | [ATTR_EXCEPTION_STACKTRACE]: error.stack,
127 | ...extraAttributes,
128 | })
129 |
130 | span.recordException(error)
131 | span.setStatus({ code: SpanStatusCode.ERROR })
132 | span.end()
133 | }
134 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 SrubbingOptions {
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 | SrubbingOptions
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 | return REGIONS[regionKey].baseUrl
138 | }
139 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/serializeAttributes.ts:
--------------------------------------------------------------------------------
1 | import { logfireApiConfig } from '.'
2 | import { 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 = alreadyScubbed ? attributes : (scrubber.scrubValue([], attributes)[0] as Record)
24 | // if the span is created through the logfire API methods, the attributes have already been scrubbed
25 |
26 | const result: SerializedAttributes = {}
27 | const nullArgs: string[] = []
28 | const schema: JSONSchema = { properties: {}, type: 'object' }
29 | for (const [key, value] of Object.entries(scrubbedAttributes)) {
30 | // we don't want to serialize the tags
31 | if (key === ATTRIBUTES_TAGS_KEY) {
32 | result[key] = value as string[]
33 | continue
34 | }
35 |
36 | if (value === null || value === undefined) {
37 | nullArgs.push(key)
38 | } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
39 | result[key] = value
40 | } else if (value instanceof Date) {
41 | result[key] = value.toISOString()
42 | } else if (Array.isArray(value)) {
43 | schema.properties[key] = {
44 | type: 'array',
45 | }
46 | result[key] = JSON.stringify(value)
47 | } else {
48 | schema.properties[key] = {
49 | type: 'object',
50 | }
51 |
52 | result[key] = JSON.stringify(value)
53 | }
54 | }
55 | if (nullArgs.length > 0) {
56 | result[JSON_NULL_FIELDS_KEY] = nullArgs
57 | }
58 | if (Object.keys(schema.properties).length > 0) {
59 | result[JSON_SCHEMA_KEY] = JSON.stringify(schema)
60 | }
61 | return result
62 | }
63 |
--------------------------------------------------------------------------------
/packages/logfire-api/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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-cf-workers/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @pydantic/logfire-cf-workers
2 |
3 | ## 0.4.3
4 |
5 | ### Patch Changes
6 |
7 | - 17dbddd: Re-export instrument function
8 |
9 | ## 0.4.2
10 |
11 | ### Patch Changes
12 |
13 | - b59e803: Bump to latest otel-cf-workers, fixes span nesting and adds header capturing
14 |
15 | ## 0.4.1
16 |
17 | ### Patch Changes
18 |
19 | - af427c5: Support for tail worker trace exporting
20 |
21 | ## 0.4.0
22 |
23 | ### Minor Changes
24 |
25 | - dc0a537: Support for EU tokens. Support span message formatting.
26 |
27 | ### Patch Changes
28 |
29 | - Updated dependencies [dc0a537]
30 | - @pydantic/logfire-api@0.4.0
31 |
32 | ## 0.3.0
33 |
34 | ### Minor Changes
35 |
36 | - 6fa1410: API updates, fixes for span kind
37 |
38 | ### Patch Changes
39 |
40 | - Updated dependencies [6fa1410]
41 | - @pydantic/logfire-api@0.3.0
42 |
43 | ## 0.2.2
44 |
45 | ### Patch Changes
46 |
47 | - 11c5ac2: Embed microlabs as a dependency
48 |
49 | ## 0.2.1
50 |
51 | ### Patch Changes
52 |
53 | - 838ba5d: Fix packages publish settings.
54 |
55 | ## 0.2.0
56 |
57 | ### Minor Changes
58 |
59 | - 0f0ce8f: Initial release.
60 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — Uncomplicated Observability — JavaScript SDK
2 |
3 | From the team behind [Pydantic](https://pydantic.dev/), **Logfire** is an observability platform built on the same belief as our
4 | open source library — that the most powerful tools can be easy to use.
5 |
6 | Check the [Github Repository README](https://github.com/pydantic/logfire-js) for more information on how to use the SDK.
7 |
--------------------------------------------------------------------------------
/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-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 | "sideEffects": false,
11 | "homepage": "https://pydantic.dev/logfire",
12 | "license": "MIT",
13 | "publishConfig": {
14 | "access": "public"
15 | },
16 | "keywords": [
17 | "logfire",
18 | "observability",
19 | "opentelemetry",
20 | "tracing",
21 | "profiling",
22 | "stats",
23 | "monitoring"
24 | ],
25 | "version": "0.4.3",
26 | "type": "module",
27 | "main": "./dist/index.cjs",
28 | "module": "./dist/index.js",
29 | "types": "./dist/index.d.ts",
30 | "exports": {
31 | ".": {
32 | "import": {
33 | "types": "./dist/index.d.ts",
34 | "default": "./dist/index.js"
35 | },
36 | "require": {
37 | "types": "./dist/index.d.cts",
38 | "default": "./dist/index.cjs"
39 | }
40 | }
41 | },
42 | "scripts": {
43 | "dev": "vite build",
44 | "build": "vite build",
45 | "lint": "eslint",
46 | "preview": "vite preview",
47 | "typecheck": "tsc",
48 | "prepack": "cp ../../LICENSE .",
49 | "postpack": "rm LICENSE"
50 | },
51 | "dependencies": {
52 | "@pydantic/logfire-api": "*",
53 | "@pydantic/otel-cf-workers": "^1.0.0-rc.51"
54 | },
55 | "devDependencies": {
56 | "@cloudflare/workers-types": "4.20250311.0",
57 | "@opentelemetry/sdk-trace-base": "^2.0.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.0.8"
65 | },
66 | "files": [
67 | "dist",
68 | "LICENSE"
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/prettier-config'
2 |
3 | export default baseConfig
4 |
--------------------------------------------------------------------------------
/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-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/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 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/exportTailEventsToLogfire.ts:
--------------------------------------------------------------------------------
1 | import { resolveBaseUrl } from '@pydantic/logfire-api'
2 |
3 | // simplified interface from CF
4 | 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 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'
2 |
3 | import { resolveBaseUrl, serializeAttributes } from '@pydantic/logfire-api'
4 | import { instrument as baseInstrument, TraceConfig } from '@pydantic/otel-cf-workers'
5 |
6 | import { TailWorkerExporter } from './TailWorkerExporter'
7 | import { ULIDGenerator } from './ULIDGenerator'
8 | export * from './exportTailEventsToLogfire'
9 |
10 | export interface CloudflareConfigOptions {
11 | baseUrl?: string
12 | token: string
13 | }
14 |
15 | type Env = Record
16 |
17 | type ConfigOptionsBase = Pick
18 |
19 | export interface InProcessConfigOptions extends ConfigOptionsBase {
20 | baseUrl?: string
21 | }
22 |
23 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
24 | export interface TailConfigOptions extends ConfigOptionsBase {}
25 |
26 | function getInProcessConfig(config: InProcessConfigOptions): (env: Env) => TraceConfig {
27 | return (env: Env): TraceConfig => {
28 | const { LOGFIRE_TOKEN: token = '' } = env
29 |
30 | const baseUrl = resolveBaseUrl(env, config.baseUrl, token)
31 |
32 | return Object.assign({}, config, {
33 | exporter: {
34 | headers: { Authorization: token },
35 | url: `${baseUrl}/v1/traces`,
36 | },
37 | idGenerator: new ULIDGenerator(),
38 | postProcessor: (spans: ReadableSpan[]) => postProcessAttributes(spans),
39 | })
40 | }
41 | }
42 |
43 | export function getTailConfig(config: TailConfigOptions): (env: Env) => TraceConfig {
44 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
45 | return (_env: Env): TraceConfig => {
46 | return Object.assign({}, config, {
47 | exporter: new TailWorkerExporter(),
48 | idGenerator: new ULIDGenerator(),
49 | })
50 | }
51 | }
52 |
53 | export function instrumentInProcess(handler: T, config: InProcessConfigOptions): T {
54 | return baseInstrument(handler, getInProcessConfig(config)) as T
55 | }
56 |
57 | export function instrumentTail(handler: T, config: TailConfigOptions): T {
58 | return baseInstrument(handler, getTailConfig(config)) as T
59 | }
60 |
61 | /**
62 | * Alias for `instrumentInProcess` to maintain compatibility with previous versions.
63 | */
64 | export const instrument = instrumentInProcess
65 |
66 | function postProcessAttributes(spans: ReadableSpan[]) {
67 | for (const span of spans) {
68 | for (const attrKey of Object.keys(span.attributes)) {
69 | const attrVal = span.attributes[attrKey]
70 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
71 | if (attrVal === undefined || attrVal === null) {
72 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
73 | delete span.attributes[attrKey]
74 | }
75 | }
76 | Object.assign(span.attributes, serializeAttributes(span.attributes))
77 | }
78 | return spans
79 | }
80 |
--------------------------------------------------------------------------------
/packages/logfire-cf-workers/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/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', '@pydantic/logfire-api'])
5 |
--------------------------------------------------------------------------------
/packages/logfire/.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/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # logfire
2 |
3 | ## 0.5.2
4 |
5 | ### Patch Changes
6 |
7 | - cd2ac40: Fix attribute serialization
8 | - Updated dependencies [cd2ac40]
9 | - @pydantic/logfire-api@0.4.1
10 |
11 | ## 0.5.1
12 |
13 | ### Patch Changes
14 |
15 | - 14833ef: Fix typo in interface name
16 |
17 | ## 0.5.0
18 |
19 | ### Minor Changes
20 |
21 | - e1dc8d0: Allow configuration of node auto instrumentations
22 |
23 | ## 0.4.1
24 |
25 | ### Patch Changes
26 |
27 | - 8dbb603: Fix for not picking up environment
28 |
29 | ## 0.4.0
30 |
31 | ### Minor Changes
32 |
33 | - dc0a537: Support for EU tokens. Support span message formatting.
34 | - 65274e3: Support us/eu tokens
35 |
36 | ### Patch Changes
37 |
38 | - Updated dependencies [dc0a537]
39 | - @pydantic/logfire-api@0.4.0
40 |
41 | ## 0.3.0
42 |
43 | ### Minor Changes
44 |
45 | - 6fa1410: API updates, fixes for span kind
46 |
47 | ### Patch Changes
48 |
49 | - Updated dependencies [6fa1410]
50 | - @pydantic/logfire-api@0.3.0
51 |
52 | ## 0.2.2
53 |
54 | ### Patch Changes
55 |
56 | - a391811: Fix for a peer package
57 |
58 | ## 0.2.1
59 |
60 | ### Patch Changes
61 |
62 | - 838ba5d: Fix packages publish settings.
63 | - Updated dependencies [838ba5d]
64 | - @pydantic/logfire-api@0.2.1
65 |
66 | ## 0.2.0
67 |
68 | ### Minor Changes
69 |
70 | - 0f0ce8f: Initial release.
71 |
72 | ### Patch Changes
73 |
74 | - Updated dependencies [0f0ce8f]
75 | - @pydantic/logfire-api@0.2.0
76 |
--------------------------------------------------------------------------------
/packages/logfire/README.md:
--------------------------------------------------------------------------------
1 | # Pydantic Logfire — Uncomplicated Observability — JavaScript SDK
2 |
3 | From the team behind [Pydantic](https://pydantic.dev/), **Logfire** is an observability platform built on the same belief as our
4 | open source library — that the most powerful tools can be easy to use.
5 |
6 | Check the [Github Repository README](https://github.com/pydantic/logfire-js) for more information on how to use the SDK.
7 |
--------------------------------------------------------------------------------
/packages/logfire/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/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logfire",
3 | "description": "JavaScript 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 | "sideEffects": false,
10 | "homepage": "https://pydantic.dev/logfire",
11 | "license": "MIT",
12 | "publishConfig": {
13 | "access": "public"
14 | },
15 | "keywords": [
16 | "logfire",
17 | "observability",
18 | "opentelemetry",
19 | "tracing",
20 | "profiling",
21 | "stats",
22 | "monitoring"
23 | ],
24 | "version": "0.5.2",
25 | "type": "module",
26 | "main": "./dist/index.cjs",
27 | "module": "./dist/index.js",
28 | "types": "./dist/index.d.ts",
29 | "exports": {
30 | ".": {
31 | "import": {
32 | "types": "./dist/index.d.ts",
33 | "default": "./dist/index.js"
34 | },
35 | "require": {
36 | "types": "./dist/index.d.cts",
37 | "default": "./dist/index.cjs"
38 | }
39 | }
40 | },
41 | "scripts": {
42 | "dev": "vite build",
43 | "build": "vite build",
44 | "lint": "eslint",
45 | "preview": "vite preview",
46 | "typecheck": "tsc",
47 | "prepack": "cp ../../LICENSE .",
48 | "postpack": "rm LICENSE",
49 | "test": "vitest --passWithNoTests"
50 | },
51 | "dependencies": {
52 | "@pydantic/logfire-api": "*"
53 | },
54 | "devDependencies": {
55 | "@opentelemetry/api": "^1.9.0",
56 | "@opentelemetry/auto-instrumentations-node": "^0.57.0",
57 | "@opentelemetry/context-async-hooks": "^2.0.0",
58 | "@opentelemetry/core": "^2.0.0",
59 | "@opentelemetry/exporter-metrics-otlp-proto": "^0.200.0",
60 | "@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
61 | "@opentelemetry/resources": "^2.0.0",
62 | "@opentelemetry/sdk-metrics": "^2.0.0",
63 | "@opentelemetry/sdk-node": "^0.200.0",
64 | "@opentelemetry/sdk-trace-base": "^2.0.0",
65 | "@pydantic/logfire-tooling-config": "*",
66 | "eslint": "^9.22.0",
67 | "prettier": "3.5.3",
68 | "typescript": "^5.8.2",
69 | "vite": "^6.2.0",
70 | "vite-plugin-dts": "^4.5.3",
71 | "vitest": "^3.1.1"
72 | },
73 | "peerDependencies": {
74 | "@opentelemetry/api": "^1.9.0",
75 | "@opentelemetry/auto-instrumentations-node": "^0.57.0",
76 | "@opentelemetry/context-async-hooks": "^2.0.0",
77 | "@opentelemetry/core": "^2.0.0",
78 | "@opentelemetry/exporter-metrics-otlp-proto": "^0.200.0",
79 | "@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
80 | "@opentelemetry/resources": "^2.0.0",
81 | "@opentelemetry/sdk-metrics": "^2.0.0",
82 | "@opentelemetry/sdk-node": "^0.200.0",
83 | "@opentelemetry/sdk-trace-base": "^2.0.0"
84 | },
85 | "files": [
86 | "dist",
87 | "LICENSE"
88 | ]
89 | }
90 |
--------------------------------------------------------------------------------
/packages/logfire/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import baseConfig from '@pydantic/logfire-tooling-config/prettier-config'
2 |
3 | export default baseConfig
4 |
--------------------------------------------------------------------------------
/packages/logfire/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 |
--------------------------------------------------------------------------------
/packages/logfire/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 |
--------------------------------------------------------------------------------
/packages/logfire/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/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './logfireConfig'
2 | export { DiagLogLevel } from '@opentelemetry/api'
3 | export * from '@pydantic/logfire-api'
4 |
--------------------------------------------------------------------------------
/packages/logfire/src/logfireConfig.ts:
--------------------------------------------------------------------------------
1 | import { DiagLogLevel } from '@opentelemetry/api'
2 | import { InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node'
3 | import { MetricReader } from '@opentelemetry/sdk-metrics'
4 | import { IdGenerator, SpanProcessor } from '@opentelemetry/sdk-trace-base'
5 | import * as logfireApi from '@pydantic/logfire-api'
6 |
7 | import { start } from './sdk'
8 | import { ULIDGenerator } from './ULIDGenerator'
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 | * Defines the available internal logging levels for the diagnostic logger.
57 | */
58 | diagLogLevel?: DiagLogLevel
59 | /**
60 | * 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.
61 | */
62 | distributedTracing?: boolean
63 | /**
64 | * 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.
65 | * Defaults to the `LOGFIRE_ENVIRONMENT` environment variable.
66 | */
67 | environment?: string
68 | /**
69 | * Set to False to disable sending all metrics, or provide a MetricsOptions object to configure metrics, e.g. additional metric readers.
70 | */
71 | metrics?: false | MetricsOptions
72 | /**
73 | * The node auto instrumentations to use. See [Node Auto Instrumentations](https://opentelemetry.io/docs/languages/js/libraries/#registration) for more information.
74 | */
75 | nodeAutoInstrumentations?: InstrumentationConfigMap
76 | /**
77 | * The otel scope to use for the logfire API. Defaults to 'logfire'.
78 | */
79 | otelScope?: string
80 | /**
81 | * Options for scrubbing sensitive data. Set to False to disable.
82 | */
83 | scrubbing?: false | logfireApi.SrubbingOptions
84 | /**
85 | * Whether to send logs to logfire.dev.
86 | * 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.
87 | */
88 | sendToLogfire?: 'if-token-present' | boolean
89 | /**
90 | * Name of this service.
91 | * Defaults to the `LOGFIRE_SERVICE_NAME` environment variable.
92 | */
93 | serviceName?: string
94 | /**
95 | * Version of this service.
96 | * Defaults to the `LOGFIRE_SERVICE_VERSION` environment variable.
97 | */
98 | serviceVersion?: string
99 | /**
100 | * The project token.
101 | * Defaults to the `LOGFIRE_TOKEN` environment variable.
102 | */
103 | token?: string
104 | }
105 |
106 | const DEFAULT_OTEL_SCOPE = 'logfire'
107 | const TRACE_ENDPOINT_PATH = 'v1/traces'
108 | const METRIC_ENDPOINT_PATH = 'v1/metrics'
109 | const DEFAULT_AUTO_INSTRUMENTATION_CONFIG: InstrumentationConfigMap = {
110 | // https://opentelemetry.io/docs/languages/js/libraries/#registration
111 | // This particular instrumentation creates a lot of noise on startup
112 | '@opentelemetry/instrumentation-fs': {
113 | enabled: false,
114 | },
115 | }
116 |
117 | export interface LogfireConfig {
118 | additionalSpanProcessors: SpanProcessor[]
119 | authorizationHeaders: Record
120 | baseUrl: string
121 | codeSource: CodeSource | undefined
122 | deploymentEnvironment: string | undefined
123 | diagLogLevel?: DiagLogLevel
124 | distributedTracing: boolean
125 | idGenerator: IdGenerator
126 | metricExporterUrl: string
127 | metrics: false | MetricsOptions | undefined
128 | nodeAutoInstrumentations: InstrumentationConfigMap
129 | otelScope: string
130 | sendToLogfire: boolean
131 | serviceName: string | undefined
132 | serviceVersion: string | undefined
133 | token: string | undefined
134 | traceExporterUrl: string
135 | }
136 |
137 | const DEFAULT_LOGFIRE_CONFIG: LogfireConfig = {
138 | additionalSpanProcessors: [],
139 | authorizationHeaders: {},
140 | baseUrl: '',
141 | codeSource: undefined,
142 | deploymentEnvironment: undefined,
143 | diagLogLevel: undefined,
144 | distributedTracing: true,
145 | idGenerator: new ULIDGenerator(),
146 | metricExporterUrl: '',
147 | metrics: undefined,
148 | nodeAutoInstrumentations: DEFAULT_AUTO_INSTRUMENTATION_CONFIG,
149 | otelScope: DEFAULT_OTEL_SCOPE,
150 | sendToLogfire: false,
151 | serviceName: process.env.LOGFIRE_SERVICE_NAME,
152 | serviceVersion: process.env.LOGFIRE_SERVICE_VERSION,
153 | token: '',
154 | traceExporterUrl: '',
155 | }
156 |
157 | export const logfireConfig: LogfireConfig = DEFAULT_LOGFIRE_CONFIG
158 |
159 | export function configure(config: LogfireConfigOptions = {}) {
160 | const { otelScope, scrubbing, ...cnf } = config
161 |
162 | const env = process.env
163 |
164 | if (otelScope) {
165 | logfireApi.configureLogfireApi({ otelScope, scrubbing })
166 | }
167 |
168 | const token = cnf.token ?? env.LOGFIRE_TOKEN
169 | const sendToLogfire = logfireApi.resolveSendToLogfire(process.env, cnf.sendToLogfire, token)
170 | const baseUrl = !sendToLogfire || !token ? '' : logfireApi.resolveBaseUrl(process.env, cnf.advanced?.baseUrl, token)
171 |
172 | Object.assign(logfireConfig, {
173 | additionalSpanProcessors: cnf.additionalSpanProcessors ?? [],
174 | authorizationHeaders: {
175 | Authorization: token ?? '',
176 | },
177 | baseUrl,
178 | codeSource: cnf.codeSource,
179 | deploymentEnvironment: cnf.environment ?? env.LOGFIRE_ENVIRONMENT,
180 | diagLogLevel: cnf.diagLogLevel,
181 | distributedTracing: resolveDistributedTracing(cnf.distributedTracing),
182 | idGenerator: cnf.advanced?.idGenerator ?? new ULIDGenerator(),
183 | metricExporterUrl: `${baseUrl}/${METRIC_ENDPOINT_PATH}`,
184 | metrics: cnf.metrics,
185 | nodeAutoInstrumentations: cnf.nodeAutoInstrumentations ?? DEFAULT_AUTO_INSTRUMENTATION_CONFIG,
186 | sendToLogfire,
187 | serviceName: cnf.serviceName ?? env.LOGFIRE_SERVICE_NAME,
188 | serviceVersion: cnf.serviceVersion ?? env.LOGFIRE_SERVICE_VERSION,
189 | token,
190 | traceExporterUrl: `${baseUrl}/${TRACE_ENDPOINT_PATH}`,
191 | })
192 |
193 | start()
194 | }
195 |
196 | function resolveDistributedTracing(option: LogfireConfigOptions['distributedTracing']) {
197 | const envDistributedTracing = process.env.LOGFIRE_DISTRIBUTED_TRACING
198 | return (option ?? envDistributedTracing === undefined) ? true : envDistributedTracing === 'true'
199 | }
200 |
--------------------------------------------------------------------------------
/packages/logfire/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 |
--------------------------------------------------------------------------------
/packages/logfire/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 { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
9 | import {
10 | ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
11 | ATTR_VCS_REPOSITORY_REF_REVISION,
12 | ATTR_VCS_REPOSITORY_URL_FULL,
13 | } from '@opentelemetry/semantic-conventions/incubating'
14 | import { reportError } from '@pydantic/logfire-api'
15 |
16 | import { logfireConfig } from './logfireConfig'
17 | import { periodicMetricReader } from './metricExporter'
18 | import { logfireSpanProcessor } from './traceExporter'
19 | import { ULIDGenerator } from './ULIDGenerator'
20 | import { removeEmptyKeys } from './utils'
21 |
22 | const LOGFIRE_ATTRIBUTES_NAMESPACE = 'logfire'
23 | const RESOURCE_ATTRIBUTES_CODE_ROOT_PATH = `${LOGFIRE_ATTRIBUTES_NAMESPACE}.code.root_path`
24 |
25 | export function start() {
26 | if (logfireConfig.diagLogLevel !== undefined) {
27 | diag.setLogger(new DiagConsoleLogger(), logfireConfig.diagLogLevel)
28 | }
29 |
30 | const resource = resourceFromAttributes(
31 | removeEmptyKeys({
32 | [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: logfireConfig.deploymentEnvironment,
33 | [ATTR_SERVICE_NAME]: logfireConfig.serviceName,
34 | [ATTR_SERVICE_VERSION]: logfireConfig.serviceVersion,
35 | [ATTR_VCS_REPOSITORY_REF_REVISION]: logfireConfig.codeSource?.revision,
36 | [ATTR_VCS_REPOSITORY_URL_FULL]: logfireConfig.codeSource?.repository,
37 | [RESOURCE_ATTRIBUTES_CODE_ROOT_PATH]: logfireConfig.codeSource?.rootPath,
38 | })
39 | ).merge(detectResources({ detectors: [envDetector] }))
40 |
41 | // use AsyncLocalStorageContextManager to manage parent <> child relationshps in async functions
42 | const contextManager = new AsyncLocalStorageContextManager()
43 |
44 | const propagator = logfireConfig.distributedTracing ? new W3CTraceContextPropagator() : undefined
45 |
46 | const processor = logfireSpanProcessor()
47 | const sdk = new NodeSDK({
48 | autoDetectResources: false,
49 | contextManager,
50 | idGenerator: new ULIDGenerator(),
51 | instrumentations: [getNodeAutoInstrumentations(logfireConfig.nodeAutoInstrumentations)],
52 | metricReader: logfireConfig.metrics === false ? undefined : periodicMetricReader(),
53 | resource,
54 | spanProcessors: [processor, ...logfireConfig.additionalSpanProcessors],
55 | textMapPropagator: propagator,
56 | })
57 |
58 | if (logfireConfig.metrics && 'additionalReaders' in logfireConfig.metrics) {
59 | const meterProvider = new MeterProvider({ readers: [periodicMetricReader(), ...logfireConfig.metrics.additionalReaders], resource })
60 | metrics.setGlobalMeterProvider(meterProvider)
61 | }
62 |
63 | sdk.start()
64 | diag.info('logfire: starting')
65 | diag.debug(JSON.stringify(logfireConfig, null, 2))
66 |
67 | process.on('uncaughtExceptionMonitor', (error: Error) => {
68 | diag.info('logfire: caught uncaught exception', error.message)
69 | reportError(error.message, error, {})
70 |
71 | // eslint-disable-next-line no-void
72 | void processor.forceFlush()
73 | })
74 |
75 | process.on('unhandledRejection', (reason: Error) => {
76 | diag.error('unhandled rejection', reason)
77 |
78 | if (reason instanceof Error) {
79 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
80 | reportError(reason.message ?? 'error', reason, {})
81 | }
82 | // eslint-disable-next-line no-void
83 | void processor.forceFlush()
84 | })
85 |
86 | // gracefully shut down the SDK on process exit
87 | process.on('SIGTERM', () => {
88 | sdk
89 | .shutdown()
90 | .catch((e: unknown) => {
91 | diag.warn('logfire SDK: error shutting down', e)
92 | })
93 | .finally(() => {
94 | diag.info('logfire SDK: shutting down')
95 | })
96 | })
97 |
98 | let _shutdown = false
99 |
100 | // eslint-disable-next-line @typescript-eslint/no-misused-promises
101 | process.on('beforeExit', async () => {
102 | if (!_shutdown) {
103 | try {
104 | await sdk.shutdown()
105 | } catch (e) {
106 | diag.warn('logfire SDK: error shutting down', e)
107 | } finally {
108 | _shutdown = true
109 | diag.info('logfire SDK: shutting down')
110 | }
111 | }
112 | })
113 | }
114 |
--------------------------------------------------------------------------------
/packages/logfire/src/traceExporter.ts:
--------------------------------------------------------------------------------
1 | import { Context } from '@opentelemetry/api'
2 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
3 | import { BatchSpanProcessor, BufferConfig, ReadableSpan, Span, SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base'
4 |
5 | import { logfireConfig } from './logfireConfig'
6 | import { VoidTraceExporter } from './VoidTraceExporter'
7 |
8 | export function logfireSpanProcessor(config?: BufferConfig) {
9 | return new LogfireSpanProcessor(new BatchSpanProcessor(traceExporter(), config))
10 | }
11 |
12 | /**
13 | * returns an OTLPTraceExporter instance pointing to the Logfire endpoint.
14 | */
15 | export function traceExporter(): SpanExporter {
16 | if (!logfireConfig.sendToLogfire) {
17 | return new VoidTraceExporter()
18 | }
19 |
20 | const token = logfireConfig.token
21 | if (!token) {
22 | // TODO: what should be done here? We're forcing sending to logfire, but we don't have a token
23 | throw new Error('Logfire token is required')
24 | }
25 |
26 | return new OTLPTraceExporter({
27 | headers: logfireConfig.authorizationHeaders,
28 | url: logfireConfig.traceExporterUrl,
29 | })
30 | }
31 |
32 | class LogfireSpanProcessor implements SpanProcessor {
33 | private wrapped: SpanProcessor
34 |
35 | constructor(wrapped: SpanProcessor) {
36 | this.wrapped = wrapped
37 | }
38 |
39 | async forceFlush(): Promise {
40 | return this.wrapped.forceFlush()
41 | }
42 |
43 | onEnd(span: ReadableSpan): void {
44 | // Note: this is too late for the regular node instrumentation. The opentelemetry API rejects the non-primitive attribute values.
45 | // Instead, the serialization happens at the `logfire.span, logfire.startSpan`, etc.
46 | // Object.assign(span.attributes, serializeAttributes(span.attributes))
47 | this.wrapped.onEnd(span)
48 | }
49 |
50 | onStart(span: Span, parentContext: Context): void {
51 | this.wrapped.onStart(span, parentContext)
52 | }
53 |
54 | async shutdown(): Promise {
55 | return this.wrapped.shutdown()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/logfire/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 |
--------------------------------------------------------------------------------
/packages/logfire/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/logfire/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/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/logfire-api'])
5 |
--------------------------------------------------------------------------------
/packages/tooling-config/eslint-config.d.mts:
--------------------------------------------------------------------------------
1 | import tseslint from 'typescript-eslint'
2 |
3 | declare const defaultExport: ReturnType
4 | export default defaultExport
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/tooling-config/prettier-config.d.mts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'prettier'
2 |
3 | declare const config: Config
4 | export default config
5 |
--------------------------------------------------------------------------------
/packages/tooling-config/prettier-config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | printWidth: 140,
3 | semi: false,
4 | singleQuote: true,
5 | trailingComma: "es5",
6 | };
7 |
--------------------------------------------------------------------------------
/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 | "noFallthroughCasesInSwitch": true,
25 | "noUncheckedSideEffectImports": true,
26 | "erasableSyntaxOnly": true
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | },
25 | plugins: [
26 | dts({
27 | // https://github.com/arethetypeswrong
28 | // https://github.com/qmhc/vite-plugin-dts/issues/267#issuecomment-1786996676
29 | afterBuild: () => {
30 | // To pass publint (`npm x publint@latest`) and ensure the
31 | // package is supported by all consumers, we must export types that are
32 | // read as ESM. To do this, there must be duplicate types with the
33 | // correct extension supplied in the package.json exports field.
34 | copyFileSync('dist/index.d.ts', 'dist/index.d.cts')
35 | },
36 | compilerOptions: { skipLibCheck: true },
37 | rollupTypes: true,
38 | staticImport: true,
39 | }),
40 | ],
41 | })
42 |
43 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------