UNPKG

@procube/pino-transmit-http

Version:

A pino browser transmit that send log statements over HTTP

161 lines (147 loc) 4.67 kB
import throttle from "lodash.throttle" import debounce from "lodash.debounce" import { encode } from "@msgpack/msgpack" import { LoggerOptions, BaseLogger } from "pino" import { gzip } from "pako" interface Options extends LoggerOptions { throttle?: number debounce?: number asJson?: boolean url: string } const defaultOptions: Options = { throttle: 500, asJson: false, url: "/log", } function exception2string(e: any) { if (typeof e === "string") { return e } if (e instanceof Error) { if (e.stack) { return e.stack } return e.message } if (typeof e.toString === "function") { return e.toString() } return "Unknown error" } function concatenateUint8Arrays(arrayOfArrays: Uint8Array[]): Uint8Array { const totalLength = arrayOfArrays.reduce( (total, arr) => total + arr.length, 0, ) const result = new Uint8Array(totalLength) let offset = 0 for (const arr of arrayOfArrays) { result.set(arr, offset) offset += arr.length } return result } export default function transmitHttp( inOpts: Partial<Options>, _logger: BaseLogger, ): Required<Options>["browser"]["transmit"] { const opts: Options = { ...defaultOptions, ...inOpts } if ( typeof window === "undefined" || !window.navigator || !window.navigator.sendBeacon ) { console.error("pino-transmit-http: sendBeacon is not available.") return undefined } let collection: any[] = [] async function rawSend(): Promise<void> { if (collection.length === 0) { return } try { console.log(`sending logs: ${JSON.stringify(collection)}`) // const collectionData = concatenateUint8Arrays( // collection.map((item) => encode(item)), // ) // const gzippedBody = gzip(encode(collectionData)) const body = opts.asJson ? JSON.stringify(collection) : encode(collection) const gzippedBody = gzip(body) collection = [] await fetch(opts.url, { method: "POST", mode: "same-origin", headers: { "Content-Encoding": "gzip", "Content-Type": opts.asJson ? "application/json" : "application/msgpack", }, body: gzippedBody, }) } catch (e) { console.error( `pino-transmit-http: failed to send logs: ${exception2string( e, )}`, ) } } function flush() { if (collection.length === 0) { return } console.log(`sending logs as flush: ${JSON.stringify(collection)}`) const collectionData = concatenateUint8Arrays( collection.map((item) => encode(item)), ) collection = [] const gzippedBody = gzip(collectionData) window.navigator.sendBeacon(opts.url, gzippedBody) } let send: () => void if (opts.debounce !== null && opts.debounce !== undefined) { send = debounce(rawSend, opts.debounce) } else if (opts.throttle !== null && opts.throttle !== undefined) { send = throttle(rawSend, opts.throttle, { trailing: true, leading: false, }) } else { console.warn( "pino-transmit-http: Either throttle or debounce option must be passed to pino-transmit-http. Falling back to throttle by %dms", defaultOptions.throttle, ) send = throttle(rawSend, defaultOptions.throttle, { trailing: true, leading: false, }) } if (typeof window !== "undefined") { window.addEventListener("unload", function onUnload() { send = flush flush() // flush logs, no await as we are in an unload event }) window.addEventListener("resize", function onResize() { flush() }) } return { level: opts.level, send: function (_level, logEvent) { try { collection.push({ time: logEvent.ts / 1000, level: logEvent.level.label, ...logEvent.messages[0], }) send() } catch (e) { console.error( `pino-transmit-http: Failed to transmit logs: ${exception2string( e, )}`, ) } }, } }