├── .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 | Logfire UI with Next.js traces 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 | --------------------------------------------------------------------------------