├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── deno.json ├── examples ├── package.json └── workers │ ├── basic-module │ └── index.ts │ ├── basic │ └── index.ts │ └── logflare │ └── index.ts ├── lib └── mod.ts ├── license ├── npm └── package.json ├── readme.md └── scripts └── build.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 4 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,yaml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ['**'] 6 | tags: ['v**'] 7 | 8 | env: 9 | DENO_VERSION: 2.0.2 10 | 11 | jobs: 12 | health: 13 | name: Health 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: denoland/setup-deno@v1 18 | with: 19 | deno-version: ${{ env.DENO_VERSION }} 20 | 21 | - run: deno lint 22 | - run: deno fmt --check 23 | - run: deno check lib/*.ts 24 | 25 | dryrun: 26 | needs: [health] 27 | name: 'Publish (dry run)' 28 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: denoland/setup-deno@v1 33 | with: 34 | deno-version: ${{ env.DENO_VERSION }} 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | registry-url: 'https://registry.npmjs.org' 39 | 40 | - run: deno task build 41 | - run: npm publish --dry-run 42 | working-directory: npm 43 | 44 | # https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry 45 | publish: 46 | needs: [health] 47 | name: Publish 48 | runs-on: ubuntu-latest 49 | if: startsWith(github.ref, 'refs/tags/v') 50 | permissions: 51 | contents: read 52 | id-token: write # -> authentication 53 | steps: 54 | - uses: actions/checkout@v4 55 | - uses: denoland/setup-deno@v1 56 | with: 57 | deno-version: ${{ env.DENO_VERSION }} 58 | - uses: actions/setup-node@v4 59 | with: 60 | node-version: 20 61 | registry-url: 'https://registry.npmjs.org' 62 | 63 | - run: deno task build 64 | 65 | - name: 'Publish → npm' 66 | if: ${{ !contains(github.ref, '-next.') }} 67 | run: npm publish --provenance --access public 68 | working-directory: npm 69 | env: 70 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 71 | 72 | - name: 'Publish → npm (pre-release)' 73 | if: ${{ contains(github.ref, '-next.') }} 74 | run: npm publish --tag next --provenance --access public 75 | working-directory: npm 76 | env: 77 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /coverage 4 | /npm/* 5 | !/npm/package.json 6 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": "./lib/mod.ts", 3 | "tasks": { 4 | "build": "deno run -A scripts/build.ts" 5 | }, 6 | "imports": { 7 | "@std/assert": "jsr:@std/assert@^1", 8 | "@std/path": "jsr:@std/path@^1", 9 | "diary": "npm:diary@^0.4.4" 10 | }, 11 | "lock": false, 12 | "lint": { 13 | "rules": { 14 | "exclude": [ 15 | "no-var", 16 | "prefer-const", 17 | "no-cond-assign", 18 | "no-inner-declarations" 19 | ] 20 | } 21 | }, 22 | "fmt": { 23 | "lineWidth": 100, 24 | "singleQuote": true, 25 | "useTabs": true 26 | }, 27 | "exclude": [ 28 | "npm", 29 | "examples" 30 | ], 31 | "publish": { 32 | "include": [ 33 | "lib/*.ts", 34 | "license", 35 | "readme.md" 36 | ], 37 | "exclude": [ 38 | "**/*.test.ts", 39 | "**/*.bench.ts" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "workers-logger": "latest" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/workers/basic-module/index.ts: -------------------------------------------------------------------------------- 1 | import type { Reporter } from 'workers-logger'; 2 | import { track } from 'workers-logger'; 3 | 4 | const reporter: Reporter = (events) => void console.log(events); 5 | 6 | // TODO: ~ Types dont exist yet — waiting on https://github.com/cloudflare/workers-types/pull/102 7 | const worker: ModuleWorker = { 8 | async fetch(request, _env, context) { 9 | const log = track(request, 'worker-example', reporter); 10 | 11 | log.info('gearing up to make a response'); 12 | 13 | const res = new Response('hi there'); 14 | 15 | context.waitUntil(log.report(res)); 16 | 17 | return res; 18 | }, 19 | }; 20 | 21 | export default worker; 22 | -------------------------------------------------------------------------------- /examples/workers/basic/index.ts: -------------------------------------------------------------------------------- 1 | import type { Reporter } from 'workers-logger'; 2 | import { track } from 'workers-logger'; 3 | 4 | const reporter: Reporter = (events) => void console.log(events); 5 | 6 | addEventListener('fetch', (event) => { 7 | const { request } = event; 8 | const log = track(request, 'worker-example', reporter); 9 | 10 | log.info('gearing up to make a response'); 11 | 12 | const res = new Response('hi there'); 13 | 14 | event.waitUntil(log.report(res)); 15 | 16 | return res; 17 | }); 18 | -------------------------------------------------------------------------------- /examples/workers/logflare/index.ts: -------------------------------------------------------------------------------- 1 | import type { Reporter } from 'workers-logger'; 2 | import { format, track } from 'workers-logger'; 3 | 4 | const logflareReport: Reporter = (events, { req, res }) => { 5 | const url = new URL(req.url); 6 | 7 | const metadata = { 8 | method: req.method, 9 | pathname: url.pathname, 10 | headers: Object.fromEntries(req.headers), 11 | response: { 12 | status: res.status, 13 | headers: Object.fromEntries(res.headers), 14 | }, 15 | log: events.map((i) => ({ 16 | level: i.level, 17 | message: format(i.message, ...i.extra), 18 | })), 19 | }; 20 | 21 | // prettier-ignore 22 | const message = `${req.headers.get('cf-connecting-ip')} (${req.headers.get('cf-ray')}) ${req.method} ${req.url} ${res.status}`; 23 | 24 | return fetch('https://api.logflare.app/logs', { 25 | method: 'POST', 26 | headers: { 27 | 'x-api-key': '', 28 | 'content-type': 'application/json', 29 | }, 30 | body: JSON.stringify({ 31 | source: '', 32 | log_entry: message, 33 | metadata, 34 | }), 35 | }); 36 | }; 37 | 38 | addEventListener('fetch', (event) => { 39 | const { request } = event; 40 | const log = track(request, 'worker-example', logflareReport); 41 | 42 | log.info('gearing up to make a response'); 43 | 44 | const res = new Response('hi there'); 45 | 46 | event.waitUntil(log.report(res)); 47 | 48 | return res; 49 | }); 50 | -------------------------------------------------------------------------------- /lib/mod.ts: -------------------------------------------------------------------------------- 1 | import { diary } from 'diary'; 2 | import type { Diary, LogEvent } from 'diary'; 3 | 4 | export type { LogEvent, LogLevels } from 'diary'; 5 | 6 | export { compare, sprintf as format } from 'diary/utils'; 7 | export { enable } from 'diary'; 8 | 9 | export type Reporter = ( 10 | events: LogEvent[], 11 | context: { req: Request; res: Response }, 12 | ) => Promise | void; 13 | 14 | export interface Tracker extends Diary { 15 | report(response: Response): Promise | void | undefined; 16 | } 17 | 18 | export const track = ( 19 | req: Request, 20 | name?: string, 21 | reporter?: Reporter, 22 | ): Tracker => { 23 | const queue: LogEvent[] = []; 24 | 25 | const $ = diary(name || '', (event) => void queue.push(event)) as Tracker; 26 | 27 | $.report = (res: Response) => { 28 | if (queue.length && typeof reporter === 'function') { 29 | return reporter(queue, { req, res }); 30 | } 31 | }; 32 | 33 | return $; 34 | }; 35 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Marais Rossouw 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. -------------------------------------------------------------------------------- /npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workers-logger", 3 | "version": "0.2.1", 4 | "repository": "maraisr/workers-logger", 5 | "description": "A fast and effective logging framework for Cloudflare Workers", 6 | "license": "MIT", 7 | "author": "Marais Rossow (https://marais.io)", 8 | "keywords": [ 9 | "logging", 10 | "tracing", 11 | "workers" 12 | ], 13 | "sideEffects": false, 14 | "exports": { 15 | ".": { 16 | "import": "./index.mjs", 17 | "require": "./index.js", 18 | "types": "./index.d.ts" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "main": "index.js", 23 | "module": "index.mjs", 24 | "types": "index.d.ts", 25 | "dependencies": { 26 | "diary": "^0.4.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | # workers-logger [![licenses](https://licenses.dev/b/npm/workers-logger?style=dark)](https://licenses.dev/npm/workers-logger) 6 | 7 | 8 | 9 | Fast and effective logging for [Cloudflare Workers](https://workers.cloudflare.com/). 10 | 11 |
12 |
13 | 14 | 15 | 16 | This is free to use software, but if you do like it, consider supporting me ❤️ 17 | 18 | [![sponsor me](https://badgen.net/badge/icon/sponsor?icon=github&label&color=gray)](https://github.com/sponsors/maraisr) 19 | [![buy me a coffee](https://badgen.net/badge/icon/buymeacoffee?icon=buymeacoffee&label&color=gray)](https://www.buymeacoffee.com/marais) 20 | 21 | 22 | 23 |
24 | 25 | ## ⚡️ Features 26 | 27 | - Super [light weight](https://npm.anvaka.com/#/view/2d/workers-logger) 28 | - Custom [Reporters](#Reporters) 29 | - Built on top of [`diary`](https://github.com/maraisr/diary) 30 | - Optimized to not hinder critical path 31 | 32 | ## ⚙️ Install 33 | 34 | ```sh 35 | npm add workers-logger 36 | ``` 37 | 38 | ## 🚀 Usage 39 | 40 | ```ts 41 | import { track } from 'workers-logger'; 42 | 43 | addEventListener('fetch', (event) => { 44 | const { request } = event; 45 | const log = track(request); 46 | 47 | log.info('gearing up to make a response'); 48 | 49 | const res = new Response('hi there'); 50 | 51 | event.waitUntil(log.report(res)); 52 | 53 | return res; 54 | }); 55 | ``` 56 | 57 | > to see more visit [examples](/examples) 58 | 59 | ## 🔎 API 60 | 61 | ### track(request: Request, name?: string, reporter?: Reporter) 62 | 63 | Returns [log functions](https://github.com/maraisr/diary#log-functions) and our 64 | [`.report`](#reportresponse-response) method. 65 | 66 | #### report(response: Response) 67 | 68 | Returns a promise with intended usage with `event.waitUntil`. And thus in terns runs your 69 | [`reporter`](#reporters) defined on track. 70 | 71 | ## Reporters 72 | 73 | A reporter is a single function ran at then end of [`.report`](#reportresponse-response). And gives 74 | you the ability to send that data somewhere, or merely into 75 | [dashboard logs](https://blog.cloudflare.com/introducing-workers-dashboard-logs/). 76 | 77 | ```ts 78 | import type { Reporter } from 'workers-logger'; 79 | import { track } from 'workers-logger'; 80 | 81 | const reporter: Reporter = (events, { req, res }) => { 82 | // do whatever you want 83 | }; 84 | 85 | addEventListener('fetch', (event) => { 86 | const { request } = event; 87 | const log = track(request, 'my-worker', reporter); 88 | 89 | log.info('gearing up to make a response'); 90 | 91 | const res = new Response('hi there'); 92 | 93 | event.waitUntil(log.report(res)); 94 | 95 | return res; 96 | }); 97 | ``` 98 | 99 | > example when sending into [Logflare](https://logflare.app/) at 100 | > [/examples/workers/logflare](/examples/workers/logflare/index.ts) 101 | 102 | ## License 103 | 104 | MIT © [Marais Rossouw](https://marais.io) 105 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | // Credit @lukeed https://github.com/lukeed/empathic/blob/main/scripts/build.ts 2 | 3 | // Publish: 4 | // -> edit package.json version 5 | // -> edit deno.json version 6 | // $ git commit "release: x.x.x" 7 | // $ git tag "vx.x.x" 8 | // $ git push origin main --tags 9 | // #-> CI builds w/ publish 10 | 11 | import oxc from 'npm:oxc-transform@^0.30'; 12 | import { parseAsync } from 'npm:oxc-parser@^0.34'; 13 | import MagicString from 'npm:magic-string'; 14 | import { join, resolve } from '@std/path'; 15 | import { type ESTreeMap, walk } from 'npm:astray@1.1.1'; 16 | 17 | import denoJson from '../deno.json' with { type: 'json' }; 18 | 19 | const outdir = resolve('npm'); 20 | 21 | let Inputs; 22 | if (typeof denoJson.exports === 'string') Inputs = { '.': denoJson.exports }; 23 | else Inputs = denoJson.exports; 24 | 25 | async function transform(name: string, filename: string) { 26 | if (name === '.') name = 'index'; 27 | name = name.replace(/^\.\//, ''); 28 | 29 | let entry = resolve(filename); 30 | let source = await Deno.readTextFile(entry); 31 | 32 | let xform = oxc.transform(entry, source, { 33 | typescript: { 34 | onlyRemoveTypeImports: true, 35 | declaration: { 36 | stripInternal: true, 37 | }, 38 | }, 39 | }); 40 | 41 | if (xform.errors.length > 0) bail('transform', xform.errors); 42 | 43 | // ESM -> CJS 44 | let s = new MagicString(xform.code); 45 | let AST = await parseAsync(xform.code).then((x) => x.program); 46 | 47 | type Location = { 48 | start: number; 49 | end: number; 50 | }; 51 | 52 | // Again, thanks @lukeed: https://github.com/lukeed/polka/blob/895ffb96945c4d40e62205bfc6897f5bfc76700e/scripts/build.ts#L47 53 | walk(AST.body, { 54 | ImportDeclaration(n) { 55 | let { start, end } = n as unknown as Location; 56 | let src = n.source as unknown as { 57 | type: 'StringLiteral'; 58 | value: string; 59 | start: number; 60 | end: number; 61 | }; 62 | 63 | let from = src.value; 64 | if (from.startsWith('node:')) { 65 | from = from.substring(5); 66 | } 67 | 68 | let $locals: string[] = []; 69 | let $default: string | undefined; 70 | 71 | let i = 0, arr = n.specifiers; 72 | let tmp: typeof arr[number]; 73 | 74 | for (; i < arr.length; i++) { 75 | tmp = arr[i]; 76 | 77 | switch (tmp.type) { 78 | case 'ImportDefaultSpecifier': 79 | case 'ImportNamespaceSpecifier': { 80 | if ($default) throw new Error('Double `default` exports!'); 81 | $default = tmp.local.name; 82 | break; 83 | } 84 | 85 | case 'ImportSpecifier': { 86 | let { imported, local } = tmp; 87 | if (imported.name !== local.name) { 88 | $locals.push(`${imported.name}: ${local.name}`); 89 | } else { 90 | $locals.push(local.name); 91 | } 92 | break; 93 | } 94 | } 95 | } 96 | 97 | let stmt = 'const '; 98 | if ($default) { 99 | stmt += $default; 100 | } 101 | if ($locals.length > 0) { 102 | if ($default) stmt += ', '; 103 | stmt += '{ ' + $locals.join(', ') + ' }'; 104 | } 105 | 106 | let qq = s.snip(src.start, src.start + 1); 107 | stmt += ` = require(${qq + from + qq});`; 108 | s.overwrite(start, end, stmt); 109 | }, 110 | ExportDefaultDeclaration(n) { 111 | let start = (n as unknown as Location).start; 112 | s.overwrite(start, start + 'export default'.length, 'module.exports ='); 113 | }, 114 | ExportNamedDeclaration(n) { 115 | let { start, end } = n as unknown as Location; 116 | let type = n.declaration?.type; 117 | let key: string | undefined; 118 | 119 | // TODO: Handle re-exports 120 | if (n.type === 'ExportNamedDeclaration' && type == null) { 121 | let src = n.source as unknown as { 122 | type: 'StringLiteral'; 123 | value: string; 124 | start: number; 125 | end: number; 126 | }; 127 | 128 | let from = src.value; 129 | 130 | let i = 0, arr = n.specifiers; 131 | let tmp: typeof arr[number]; 132 | 133 | let $locals: string[] = []; 134 | let $exports: string[] = []; 135 | 136 | for (; i < arr.length; i++) { 137 | tmp = arr[i]; 138 | 139 | let { local, exported } = tmp; 140 | let lcl = `__CJS_${exported.name}`; 141 | $locals.push(`${local.name}: ${lcl}`); 142 | $exports.push(`${exported.name} = ${lcl}`); 143 | } 144 | 145 | invariant($locals.length > 0, 'No locals found'); 146 | 147 | let stmt = 'const '; 148 | stmt += '{ ' + $locals.join(', ') + ' }'; 149 | 150 | let qq = s.snip(src.start, src.start + 1); 151 | stmt += ` = require(${qq + from + qq});`; 152 | for (i = 0; i < $exports.length; i++) { 153 | stmt += `\nexports.${$exports[i]};`; 154 | } 155 | s.overwrite(start, end, stmt); 156 | 157 | return; 158 | } 159 | 160 | if (type === 'FunctionDeclaration') { 161 | key = (n.declaration as ESTreeMap['FunctionDeclaration']).id?.name; 162 | } else if (type === 'VariableDeclaration') { 163 | let decl = (n.declaration as ESTreeMap['VariableDeclaration']).declarations.find((d) => 164 | d.type === 'VariableDeclarator' 165 | ); 166 | key = (decl?.id as ESTreeMap['Identifier'])?.name; 167 | } 168 | 169 | if (!key) console.log('EXPORT NAMED TYPE?', n); 170 | 171 | if (key) { 172 | s.remove(start, start + 'export '.length); 173 | s.append(`\nexports.${key} = ${key};`); 174 | } 175 | }, 176 | }); 177 | 178 | write(`${name}.d.ts`, xform.declaration!); 179 | write(`${name}.mjs`, xform.code); 180 | write(`${name}.js`, s.toString()); 181 | } 182 | 183 | console.log('! cleaning "npm" directory'); 184 | await new Deno.Command('git', { 185 | args: ['clean', '-xfd', outdir], 186 | stderr: 'inherit', 187 | }).output(); 188 | 189 | for (let [name, filename] of Object.entries(Inputs)) await transform(name, filename); 190 | 191 | await copy('readme.md'); 192 | await copy('license'); 193 | 194 | // --- 195 | 196 | function bail(label: string, errors: string[]): never { 197 | console.error('[%s] error(s)\n', label, errors.join('')); 198 | Deno.exit(1); 199 | } 200 | 201 | function exists(path: string) { 202 | try { 203 | Deno.statSync(path); 204 | return true; 205 | } catch (_) { 206 | return false; 207 | } 208 | } 209 | 210 | function copy(file: string) { 211 | if (exists(file)) { 212 | let outfile = join(outdir, file); 213 | console.log('> writing "%s" file', outfile); 214 | return Deno.copyFile(file, outfile); 215 | } 216 | } 217 | 218 | function write(fname: string, content: string) { 219 | let outfile = join(outdir, fname); 220 | console.log('> writing "%s" file', outfile); 221 | return Deno.writeTextFile(outfile, content); 222 | } 223 | 224 | function invariant(condition: boolean, message?: string) { 225 | if (!condition) throw new Error(message); 226 | } 227 | --------------------------------------------------------------------------------