├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── build.yml │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── cli.ts ├── index.ts └── transport.ts ├── test └── logger.test.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:@typescript-eslint/recommended"], 4 | "plugins": ["@typescript-eslint"], 5 | "parserOptions": { 6 | "ecmaVersion": 2018, 7 | "sourceType": "module" 8 | }, 9 | "rules": { 10 | "semi": ["error", "always"], 11 | "indent": "off", 12 | "@typescript-eslint/explicit-function-return-type": "off", 13 | "@typescript-eslint/no-explicit-any": "off", 14 | "@typescript-eslint/indent": ["error", 2] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build TypeScript 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | name: Build package 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Setup node 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 14 16 | cache: npm 17 | - run: npm ci 18 | - run: npm run-script build 19 | 20 | # vim:ts=2:sw=2:et 21 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub 2 | # Packages when a release is created 3 | # 4 | # For more information see: 5 | # https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 6 | 7 | name: Publish Node.js Package 8 | 9 | on: 10 | push: 11 | tags: 12 | - "*.*.*" 13 | 14 | env: 15 | NODE_VERSION: 14 16 | 17 | jobs: 18 | publish-npm: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ env.NODE_VERSION }} 25 | registry-url: https://registry.npmjs.org/ 26 | - run: npm ci 27 | - run: npm version --new-version ${GITHUB_REF#refs/tags/} --no-git-tag-version 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | 32 | # vim:ft=yaml:et:ts=2:sw=2 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | # 7 | node_modules 8 | yarn.lock 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | # VSCode 13 | .vscode 14 | 15 | # Specific 16 | dist 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !/LICENSE 3 | !/README.md 4 | !/dist/**/* 5 | !/package.json 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrew Avdeev 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 | # pino-sentry 2 | 3 | [![node](https://img.shields.io/badge/node-10.0+-brightgreen.svg)][node-url] 4 | [![license](https://img.shields.io/github/license/aandrewww/pino-sentry.svg)][license-url] 5 | 6 | Load [pino](https://github.com/pinojs/pino) logs into [Sentry](https://sentry.io/) 7 | 8 | ## Index 9 | 10 | - [Install](#install) 11 | - [Usage](#usage) 12 | - [CLI](#cli) 13 | - [API](#api) 14 | - [Options](#options-options) 15 | - [Transport options](#transport-options) 16 | - [Log Level Mapping](#log-level-mapping) 17 | - [License](#license) 18 | 19 | ## Install 20 | 21 | ```bash 22 | npm install pino-sentry -g 23 | ``` 24 | 25 | Note: The v7 version of the Sentry JavaScript SDK requires a self-hosted 26 | version of Sentry 20.6.0 or higher. If you are using a version of self-hosted 27 | Sentry (aka onpremise) older than 20.6.0 then you will need to upgrade. See 28 | [sentry-javascript@7.0.0] release notes. 29 | 30 | [sentry-javascript@7.0.0]: https://github.com/getsentry/sentry-javascript/releases/tag/7.0.0 31 | 32 | ## Usage 33 | 34 | ### CLI 35 | 36 | ```bash 37 | node ./app.js | pino-sentry --dsn=https://******@sentry.io/12345 38 | ``` 39 | 40 | ### API 41 | 42 | ```js 43 | const { createWriteStream, Sentry } = require("pino-sentry"); 44 | // ... 45 | const opts = { 46 | /* ... */ 47 | }; 48 | const stream = createWriteStream({ dsn: process.env.SENTRY_DSN }); 49 | const logger = pino(opts, stream); 50 | 51 | // add tags 52 | logger.info({ tags: { foo: "bar" }, msg: "Error" }); 53 | 54 | // add extra 55 | logger.info({ extra: { foo: "bar" }, msg: "Error" }); 56 | 57 | // add breadcrumbs 58 | // https://docs.sentry.io/platforms/node/enriching-events/breadcrumbs/ 59 | logger.info({ 60 | msg: "Error", 61 | breadcrumbs: [ 62 | { 63 | category: "auth", 64 | message: "Authenticated user " + user.email, 65 | level: "info", 66 | }, 67 | ], 68 | }); 69 | 70 | // the sentry instance is exposed and can be used to manipulate the same sentry than pino-sentry 71 | Sentry.addBreadcrumb({ 72 | category: "custom-logger", 73 | message: "Hey there!", 74 | level: "debug", 75 | type: "debug", 76 | data: { some: "data" }, 77 | }); 78 | ``` 79 | 80 | ## Options (`options`) 81 | 82 | ### Override Message Attributes 83 | 84 | In case the generated message does not follow the standard convention, the main attribute keys can be mapped to different values, when the stream gets created. Following attribute keys can be overridden: 85 | 86 | - `msg` - the field used to get the message, it can be dot notted (eg 'data.msg') 87 | - `extra` 88 | - `stack` - the field used to get the stack, it can be dot notted (eg 'err.stack') 89 | - `maxValueLength` - option to adjust max string length for values, default is 250 90 | - `decorateScope` - option to decorate, manipulate the sentry scope just before the capture 91 | - `sentryExceptionLevels` - option that represent the levels that will be handled as exceptions. Default : `error` and `fatal` 92 | 93 | ```js 94 | const { createWriteStream, Severity } = require("pino-sentry"); 95 | // ... 96 | const opts = { 97 | /* ... */ 98 | }; 99 | const stream = createWriteStream({ 100 | dsn: process.env.SENTRY_DSN, 101 | messageAttributeKey: "message", 102 | stackAttributeKey: "trace", 103 | extraAttributeKeys: ["req", "context"], 104 | maxValueLength: 250, 105 | sentryExceptionLevels: [ 106 | Severity.Warning, 107 | Severity.Error, 108 | Severity.Fatal, 109 | ], 110 | decorateScope: (data, scope) => { 111 | scope.setUser("userId", { id: data.userId }); 112 | }, 113 | }); 114 | const logger = pino(opts, stream); 115 | ``` 116 | 117 | ### Transport options 118 | 119 | - `--dsn` (`-d`): your Sentry DSN or Data Source Name (defaults to `process.env.SENTRY_DSN`) 120 | - `--environment` (`-e`): (defaults to `process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'production'`) 121 | - `--serverName` (`-n`): transport name (defaults to `pino-sentry`) 122 | - `--debug` (`-dm`): turns debug mode on or off (default to `process.env.SENTRY_DEBUG || false`) 123 | - `--sampleRate` (`-sr`): sample rate as a percentage of events to be sent in the range of 0.0 to 1.0 (default to `1.0`) 124 | - `--maxBreadcrumbs` (`-mx`): total amount of breadcrumbs that should be captured (default to `100`) 125 | - `--level` (`-l`): minimum level for a log to be reported to Sentry (default to `debug`) 126 | 127 | ### Log Level Mapping 128 | 129 | Pino logging levels are mapped by default to Sentry's acceptable levels. 130 | 131 | ```js 132 | { 133 | trace: 'debug', 134 | debug: 'debug', 135 | info: 'info', 136 | warn: 'warning', 137 | error: 'error', 138 | fatal: 'fatal' 139 | } 140 | ``` 141 | 142 | ## License 143 | 144 | [MIT License][license-url] 145 | 146 | [license-url]: LICENSE 147 | [node-url]: https://nodejs.org 148 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | import type { Config } from "@jest/types"; 6 | 7 | const config: Config.InitialOptions = { 8 | preset: "ts-jest", 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pino-sentry", 3 | "description": "@sentry/node transport for pino logger", 4 | "author": "Andrew Avdeev ", 5 | "keywords": [ 6 | "logger", 7 | "loggly", 8 | "logging", 9 | "node", 10 | "tools", 11 | "sentry", 12 | "transport", 13 | "pino" 14 | ], 15 | "main": "dist/index.js", 16 | "types": "dist/index.d.ts", 17 | "bin": { 18 | "pino-sentry": "dist/cli.js" 19 | }, 20 | "scripts": { 21 | "clean": "rm -rf dist", 22 | "build": "tsc -b tsconfig.build.json", 23 | "lint": "eslint src/**/*.ts", 24 | "test": "jest", 25 | "prepublishOnly": "npm run lint && npm run build && npm run test" 26 | }, 27 | "repository": "https://github.com/aandrewww/pino-sentry.git", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/aandrewww/pino-sentry/issues" 31 | }, 32 | "homepage": "https://github.com/aandrewww/pino-sentry#readme", 33 | "engines": { 34 | "node": ">=10" 35 | }, 36 | "dependencies": { 37 | "@sentry/node": "^7.119.1", 38 | "commander": "^2.20.0", 39 | "pumpify": "^2.0.1", 40 | "split2": "^3.1.1", 41 | "through2": "^3.0.1" 42 | }, 43 | "devDependencies": { 44 | "@jest/types": "^28.1.1", 45 | "@types/jest": "^28.1.1", 46 | "@types/node": "^12.6.9", 47 | "@types/pino": "^6.3.12", 48 | "@types/pumpify": "^1.4.1", 49 | "@types/split2": "^2.1.6", 50 | "@types/through2": "^2.0.34", 51 | "@typescript-eslint/eslint-plugin": "^2.34.0", 52 | "@typescript-eslint/parser": "^2.34.0", 53 | "eslint": "^6.8.0", 54 | "jest": "^28.1.1", 55 | "pino": "^6.5.1", 56 | "ts-jest": "^28.0.4", 57 | "ts-node": "^10.8.1", 58 | "typescript": "^4.7.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import program from 'commander'; 4 | 5 | // import pkg from '../package.json'; 6 | import { createWriteStream } from './transport'; 7 | 8 | // main cli logic 9 | function main() { 10 | program 11 | // .version(pkg.version) 12 | .option('-d, --dsn ', 'Your Sentry DSN or Data Source Name') 13 | .option('-e, --environment ', 'Sentry environment') 14 | .option('-n, --serverName ', 'Transport name') 15 | .option('-dm, --debug ', 'Turns debug mode on or off') 16 | .option('-sr, --sampleRate ', 'Sample rate as a percentage of events to be sent in the range of 0.0 to 1.0') 17 | .option('-mb, --maxBreadcrumbs ', 'Total amount of breadcrumbs that should be captured') 18 | .option('-di, --dist ', 'Sets the distribution for all events') 19 | .option('--maxValueLength ', 'Maximum number of chars a single value can have before it will be truncated.') 20 | .option('--release ', 'The release identifier used when uploading respective source maps.') 21 | .option('-l, --level ', 'The minimum level for a log to be reported to Sentry') 22 | .action(({ dsn, serverName, environment, debug, sampleRate, maxBreadcrumbs, dist, maxValueLength, release, level }) => { 23 | try { 24 | const writeStream = createWriteStream({ 25 | dsn, 26 | serverName, 27 | environment, 28 | debug, 29 | sampleRate, 30 | maxBreadcrumbs, 31 | dist, 32 | maxValueLength, 33 | release, 34 | level, 35 | }); 36 | // Echo to stdout 37 | process.stdin.pipe(process.stdout); 38 | // Pipe to writeStream 39 | process.stdin.pipe(writeStream); 40 | console.info('[pino-sentry] Logging Initialized'); 41 | } catch (error) { 42 | console.log('[pino-sentry]', error); 43 | process.exit(1); 44 | } 45 | }); 46 | 47 | program.parse(process.argv); 48 | } 49 | 50 | main(); 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createWriteStream, 3 | createWriteStreamAsync, 4 | SentryInstance as Sentry, 5 | PinoSentryOptions, 6 | Severity, 7 | } from './transport'; 8 | -------------------------------------------------------------------------------- /src/transport.ts: -------------------------------------------------------------------------------- 1 | import stream from 'stream'; 2 | import { AsyncResource } from 'async_hooks'; 3 | import split from 'split2'; 4 | import Pump from 'pumpify'; 5 | import through from 'through2'; 6 | import * as Sentry from '@sentry/node'; 7 | import { Breadcrumb } from '@sentry/types'; 8 | 9 | type ValueOf = T extends any[] ? T[number] : T[keyof T] 10 | 11 | export const SentryInstance = Sentry; 12 | class ExtendedError extends Error { 13 | public constructor(info: any) { 14 | super(info.message); 15 | 16 | this.name = "Error"; 17 | this.stack = info.stack || null; 18 | } 19 | } 20 | 21 | // Local enum declaration, as @sentry/node deprecated using enums over strings for bundle size 22 | export enum Severity { 23 | Fatal = "fatal", 24 | Error = "error", 25 | Warning = "warning", 26 | Log = "log", 27 | Info = "info", 28 | Debug = "debug", 29 | // @deprecated: "critical" is not present in sentry 7.x sdk 30 | // https://github.com/getsentry/sentry-javascript/issues/3067 31 | Critical = "critical", 32 | } 33 | 34 | const SEVERITIES_MAP = { 35 | 10: Severity.Debug, // pino: trace 36 | 20: Severity.Debug, // pino: debug 37 | 30: Severity.Info, // pino: info 38 | 40: Severity.Warning, // pino: warn 39 | 50: Severity.Error, // pino: error 40 | 60: Severity.Fatal, // pino: fatal 41 | // Support for useLevelLabels 42 | // https://github.com/pinojs/pino/blob/master/docs/api.md#uselevellabels-boolean 43 | trace: Severity.Debug, 44 | debug: Severity.Debug, 45 | info: Severity.Info, 46 | warning: Severity.Warning, 47 | error: Severity.Error, 48 | fatal: Severity.Fatal, 49 | } as const; 50 | 51 | // How severe the Severity is 52 | const SeverityIota = { 53 | [Severity.Debug]: 1, 54 | [Severity.Log]: 2, 55 | [Severity.Info]: 3, 56 | [Severity.Warning]: 4, 57 | [Severity.Error]: 5, 58 | [Severity.Fatal]: 6, 59 | [Severity.Critical]: 7, 60 | } as const; 61 | 62 | export interface PinoSentryOptions extends Sentry.NodeOptions { 63 | /** Minimum level for a log to be reported to Sentry from pino-sentry */ 64 | level?: keyof typeof SeverityIota; 65 | messageAttributeKey?: string; 66 | extraAttributeKeys?: string[]; 67 | stackAttributeKey?: string; 68 | maxValueLength?: number; 69 | sentryExceptionLevels?: Severity[]; 70 | decorateScope?: (data: Record, _scope: Sentry.Scope) => void; 71 | } 72 | 73 | function get(data: any, path: string) { 74 | return path.split('.').reduce((acc, part) => acc && acc[part], data); 75 | } 76 | 77 | export class PinoSentryTransport { 78 | // Default minimum log level to `debug` 79 | minimumLogLevel: ValueOf = SeverityIota[Severity.Debug]; 80 | messageAttributeKey = 'msg'; 81 | extraAttributeKeys = ['extra']; 82 | stackAttributeKey = 'stack'; 83 | maxValueLength = 250; 84 | sentryExceptionLevels = [Severity.Fatal, Severity.Error]; 85 | decorateScope = (_data: Record, _scope: Sentry.Scope) => {/**/}; 86 | 87 | public constructor(options?: PinoSentryOptions) { 88 | Sentry.init(this.validateOptions(options || {})); 89 | } 90 | 91 | public getLogSeverity(level: keyof typeof SEVERITIES_MAP): Severity { 92 | return SEVERITIES_MAP[level] || Severity.Info; 93 | } 94 | 95 | public get sentry() { 96 | return Sentry; 97 | } 98 | 99 | public transformer(): stream.Transform { 100 | return through.obj((chunk: any, _enc: any, cb: any) => { 101 | this.prepareAndGo(chunk, cb); 102 | }); 103 | } 104 | 105 | public prepareAndGo(chunkInfo: ChunkInfo, cb: any): void { 106 | chunkInfo.run((chunk) => { 107 | this.chunkInfoCallback(chunk, cb); 108 | }); 109 | } 110 | 111 | private chunkInfoCallback(chunk: any, cb: any) { 112 | const severity = this.getLogSeverity(chunk.level); 113 | 114 | // Check if we send this Severity to Sentry 115 | if (!this.shouldLog(severity)) { 116 | setImmediate(cb); 117 | return; 118 | } 119 | 120 | const tags = chunk.tags || {}; 121 | const breadcrumbs: Breadcrumb[] = chunk.breadcrumbs || {}; 122 | 123 | if (chunk.reqId) { 124 | tags.uuid = chunk.reqId; 125 | } 126 | 127 | if (chunk.responseTime) { 128 | tags.responseTime = chunk.responseTime; 129 | } 130 | 131 | if (chunk.hostname) { 132 | tags.hostname = chunk.hostname; 133 | } 134 | 135 | const extra: any = {}; 136 | this.extraAttributeKeys.forEach((key: string) => { 137 | const value = get(chunk, key); 138 | if(value !== undefined) { 139 | extra[key] = value; 140 | } 141 | }); 142 | const message: any & Error = get(chunk, this.messageAttributeKey); 143 | const stack = get(chunk, this.stackAttributeKey) || ''; 144 | 145 | const scope = new Sentry.Scope(); 146 | this.decorateScope(chunk, scope); 147 | 148 | scope.setLevel(severity as any); 149 | 150 | if (this.isObject(tags)) { 151 | Object.keys(tags).forEach(tag => scope.setTag(tag, tags[tag])); 152 | } 153 | 154 | if (this.isObject(extra)) { 155 | Object.keys(extra).forEach(ext => scope.setExtra(ext, extra[ext])); 156 | } 157 | 158 | if (this.isObject(breadcrumbs)) { 159 | Object.values(breadcrumbs).forEach(breadcrumb => scope.addBreadcrumb(breadcrumb)); 160 | } 161 | 162 | // Capturing Errors / Exceptions 163 | if (this.isSentryException(severity)) { 164 | const error = message instanceof Error ? message : new ExtendedError({ message, stack }); 165 | 166 | Sentry.captureException(error, scope); 167 | setImmediate(cb); 168 | } else { 169 | // Capturing Messages 170 | Sentry.captureMessage(message, scope); 171 | setImmediate(cb); 172 | } 173 | } 174 | 175 | private validateOptions(options: PinoSentryOptions): PinoSentryOptions { 176 | const dsn = options.dsn || process.env.SENTRY_DSN; 177 | 178 | if (!dsn) { 179 | console.log('Warning: [pino-sentry] Sentry DSN must be supplied, otherwise logs will not be reported. Pass via options or `SENTRY_DSN` environment variable.'); 180 | } 181 | 182 | if (options.level) { 183 | const allowedLevels = Object.keys(SeverityIota); 184 | 185 | if (!allowedLevels.includes(options.level)) { 186 | throw new Error(`[pino-sentry] Option \`level\` must be one of: ${allowedLevels.join(', ')}. Received: ${options.level}`); 187 | } 188 | 189 | // Set minimum log level 190 | this.minimumLogLevel = SeverityIota[options.level]; 191 | } 192 | 193 | this.stackAttributeKey = options.stackAttributeKey ?? this.stackAttributeKey; 194 | this.extraAttributeKeys = options.extraAttributeKeys ?? this.extraAttributeKeys; 195 | this.messageAttributeKey = options.messageAttributeKey ?? this.messageAttributeKey; 196 | this.maxValueLength = options.maxValueLength ?? this.maxValueLength; 197 | this.sentryExceptionLevels = options.sentryExceptionLevels ?? this.sentryExceptionLevels; 198 | this.decorateScope = options.decorateScope ?? this.decorateScope; 199 | 200 | return { 201 | dsn, 202 | // npm_package_name will be available if ran with 203 | // from a "script" field in package.json. 204 | serverName: process.env.npm_package_name || 'pino-sentry', 205 | environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'production', 206 | debug: !!process.env.SENTRY_DEBUG || false, 207 | sampleRate: 1.0, 208 | maxBreadcrumbs: 100, 209 | ...options, 210 | }; 211 | } 212 | 213 | private isObject(obj: any): boolean { 214 | const type = typeof obj; 215 | return type === 'function' || type === 'object' && !!obj; 216 | } 217 | 218 | private isSentryException(level: Severity): boolean { 219 | return this.sentryExceptionLevels.includes(level); 220 | } 221 | 222 | private shouldLog(severity: Severity): boolean { 223 | const logLevel = SeverityIota[severity]; 224 | return logLevel >= this.minimumLogLevel; 225 | } 226 | } 227 | 228 | class ChunkInfo extends AsyncResource { 229 | constructor(private readonly chunk: any) { 230 | super("ChunkInfo"); 231 | } 232 | 233 | run(callback: (...args: any[]) => R, ...args: T): R { 234 | try { 235 | return this.runInAsyncScope(callback, undefined, this.chunk, ...args); 236 | } finally { 237 | this.emitDestroy(); 238 | } 239 | } 240 | } 241 | 242 | export function createWriteStream(options?: PinoSentryOptions): stream.Duplex { 243 | const transport = new PinoSentryTransport(options); 244 | const sentryTransformer = transport.transformer(); 245 | 246 | return new Pump( 247 | split((line) => { 248 | try { 249 | return new ChunkInfo(JSON.parse(line)); 250 | } catch (e) { 251 | // Returning undefined will not run the sentryTransformer 252 | return; 253 | } 254 | }), 255 | sentryTransformer 256 | ); 257 | } 258 | 259 | // Duplicate to not break API 260 | export const createWriteStreamAsync = createWriteStream; 261 | -------------------------------------------------------------------------------- /test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import pinoLogger from "pino"; 2 | import { createWriteStream } from "../src"; 3 | 4 | test('Test logger creation', () => { 5 | const SENTRY_DSN = "https://123@123.ingest.sentry.io/123"; 6 | 7 | const options = { 8 | level: "info" 9 | }; 10 | 11 | const stream = createWriteStream({ dsn: SENTRY_DSN }); 12 | 13 | const logger = pinoLogger(options, stream); 14 | 15 | logger.info('testtt info log'); 16 | logger.error('testtt log'); 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "inlineSources": true, 7 | "lib": ["es2018"], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "pretty": true, 17 | "sourceMap": true, 18 | "strict": true, 19 | "target": "es2018", // Node 10. https://stackoverflow.com/a/57607634/6635914 20 | "strictBindCallApply": false, 21 | "resolveJsonModule": true, 22 | "typeRoots": [ 23 | "@types", 24 | "node_modules/@types" 25 | ] 26 | }, 27 | "moduleResolution": "node" 28 | } 29 | --------------------------------------------------------------------------------