@procube/pino-transmit-http
Version:
A pino browser transmit that send log statements over HTTP
161 lines (147 loc) • 4.67 kB
text/typescript
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,
)}`,
)
}
},
}
}