Logger improvements (#7729)
* Remove enablement * Refactor context and filtering * Fix imports, simplify transports config * Migrate usages of debug context * Re-org, add colors and grouping to console logging * Remove temp default context * Remove manual prefix * Move colorizing out of console transport body * Reduce reuse * Pass through context * Ensure bitdrift is enabled in dev * Enable Sentry on web only * Clean up types * Docs * Format * Update tests * Clean up tests * No positional args * Revert Sentry changes * Clean up context, use it, pass metadata through to Bitdrift * Fix up debugging * Clean up metadata before passing to Bitdrift * Correct transports * Reserve context prop on metadata and include in transports * Update tests
This commit is contained in:
+78
-216
@@ -1,213 +1,89 @@
|
||||
import format from 'date-fns/format'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {DebugContext} from '#/logger/debugContext'
|
||||
import {add} from '#/logger/logDump'
|
||||
import {Sentry} from '#/logger/sentry'
|
||||
import * as env from '#/env'
|
||||
import {createBitdriftTransport} from './bitdriftTransport'
|
||||
import {Metadata} from './types'
|
||||
import {ConsoleTransportEntry, LogLevel, Transport} from './types'
|
||||
import {bitdriftTransport} from '#/logger/transports/bitdrift'
|
||||
import {consoleTransport} from '#/logger/transports/console'
|
||||
import {sentryTransport} from '#/logger/transports/sentry'
|
||||
import {LogContext, LogLevel, Metadata, Transport} from '#/logger/types'
|
||||
import {enabledLogLevels} from '#/logger/util'
|
||||
|
||||
export {LogLevel}
|
||||
export type {ConsoleTransportEntry, Transport}
|
||||
|
||||
const enabledLogLevels: {
|
||||
[key in LogLevel]: LogLevel[]
|
||||
} = {
|
||||
[LogLevel.Debug]: [
|
||||
LogLevel.Debug,
|
||||
LogLevel.Info,
|
||||
LogLevel.Log,
|
||||
LogLevel.Warn,
|
||||
LogLevel.Error,
|
||||
],
|
||||
[LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error],
|
||||
[LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error],
|
||||
[LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error],
|
||||
[LogLevel.Error]: [LogLevel.Error],
|
||||
}
|
||||
|
||||
export function prepareMetadata(metadata: Metadata): Metadata {
|
||||
return Object.keys(metadata).reduce((acc, key) => {
|
||||
let value = metadata[key]
|
||||
if (value instanceof Error) {
|
||||
value = value.toString()
|
||||
const TRANSPORTS: Transport[] = (function configureTransports() {
|
||||
switch (process.env.NODE_ENV) {
|
||||
case 'production': {
|
||||
return [sentryTransport, bitdriftTransport].filter(Boolean) as Transport[]
|
||||
}
|
||||
return {...acc, [key]: value}
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in dev mode to nicely log to the console
|
||||
*/
|
||||
export const consoleTransport: Transport = (
|
||||
level,
|
||||
message,
|
||||
metadata,
|
||||
timestamp,
|
||||
) => {
|
||||
const extra = Object.keys(metadata).length
|
||||
? ' ' + JSON.stringify(prepareMetadata(metadata), null, ' ')
|
||||
: ''
|
||||
const log = {
|
||||
[LogLevel.Debug]: console.debug,
|
||||
[LogLevel.Info]: console.info,
|
||||
[LogLevel.Log]: console.log,
|
||||
[LogLevel.Warn]: console.warn,
|
||||
[LogLevel.Error]: console.error,
|
||||
}[level]
|
||||
|
||||
if (message instanceof Error) {
|
||||
console.info(
|
||||
`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`,
|
||||
)
|
||||
log(message)
|
||||
} else {
|
||||
log(`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const sentryTransport: Transport = (
|
||||
level,
|
||||
message,
|
||||
{type, tags, ...metadata},
|
||||
timestamp,
|
||||
) => {
|
||||
const meta = prepareMetadata(metadata)
|
||||
|
||||
/**
|
||||
* If a string, report a breadcrumb
|
||||
*/
|
||||
if (typeof message === 'string') {
|
||||
const severity = (
|
||||
{
|
||||
[LogLevel.Debug]: 'debug',
|
||||
[LogLevel.Info]: 'info',
|
||||
[LogLevel.Log]: 'log', // Sentry value here is undefined
|
||||
[LogLevel.Warn]: 'warning',
|
||||
[LogLevel.Error]: 'error',
|
||||
} as const
|
||||
)[level]
|
||||
|
||||
Sentry.addBreadcrumb({
|
||||
message,
|
||||
data: meta,
|
||||
type: type || 'default',
|
||||
level: severity,
|
||||
timestamp: timestamp / 1000, // Sentry expects seconds
|
||||
})
|
||||
|
||||
// We don't want to send any network errors to sentry
|
||||
if (isNetworkError(message)) {
|
||||
return
|
||||
case 'test': {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Send all higher levels with `captureMessage`, with appropriate severity
|
||||
* level
|
||||
*/
|
||||
if (level === 'error' || level === 'warn' || level === 'log') {
|
||||
const messageLevel = ({
|
||||
[LogLevel.Log]: 'log',
|
||||
[LogLevel.Warn]: 'warning',
|
||||
[LogLevel.Error]: 'error',
|
||||
}[level] || 'log') as Sentry.Breadcrumb['level']
|
||||
// Defer non-critical messages so they're sent in a batch
|
||||
queueMessageForSentry(message, {
|
||||
level: messageLevel,
|
||||
tags,
|
||||
extra: meta,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* It's otherwise an Error and should be reported with captureException
|
||||
*/
|
||||
Sentry.captureException(message, {
|
||||
tags,
|
||||
extra: meta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const queuedMessages: [string, Parameters<typeof Sentry.captureMessage>[1]][] =
|
||||
[]
|
||||
let sentrySendTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
function queueMessageForSentry(
|
||||
message: string,
|
||||
captureContext: Parameters<typeof Sentry.captureMessage>[1],
|
||||
) {
|
||||
queuedMessages.push([message, captureContext])
|
||||
if (!sentrySendTimeout) {
|
||||
// Throttle sending messages with a leading delay
|
||||
// so that we can get Sentry out of the critical path.
|
||||
sentrySendTimeout = setTimeout(() => {
|
||||
sentrySendTimeout = null
|
||||
sendQueuedMessages()
|
||||
}, 7000)
|
||||
}
|
||||
}
|
||||
function sendQueuedMessages() {
|
||||
while (queuedMessages.length > 0) {
|
||||
const record = queuedMessages.shift()
|
||||
if (record) {
|
||||
Sentry.captureMessage(record[0], record[1])
|
||||
default: {
|
||||
return [consoleTransport]
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
/**
|
||||
* Main class. Defaults are provided in the constructor so that subclasses are
|
||||
* technically possible, if we need to go that route in the future.
|
||||
*/
|
||||
export class Logger {
|
||||
LogLevel = LogLevel
|
||||
DebugContext = DebugContext
|
||||
static Level = LogLevel
|
||||
static Context = LogContext
|
||||
|
||||
enabled: boolean
|
||||
level: LogLevel
|
||||
transports: Transport[] = []
|
||||
context: LogContext | undefined = undefined
|
||||
contextFilter: string = ''
|
||||
|
||||
protected debugContextRegexes: RegExp[] = []
|
||||
protected transports: Transport[] = []
|
||||
|
||||
constructor({
|
||||
enabled = process.env.NODE_ENV !== 'test',
|
||||
level = env.LOG_LEVEL as LogLevel,
|
||||
debug = env.LOG_DEBUG || '',
|
||||
}: {
|
||||
enabled?: boolean
|
||||
level?: LogLevel
|
||||
debug?: string
|
||||
} = {}) {
|
||||
this.enabled = enabled !== false
|
||||
this.level = debug ? LogLevel.Debug : level ?? LogLevel.Info // default to info
|
||||
this.debugContextRegexes = (debug || '').split(',').map(context => {
|
||||
return new RegExp(context.replace(/[^\w:*]/, '').replace(/\*/g, '.*'))
|
||||
static create(context?: LogContext) {
|
||||
const logger = new Logger({
|
||||
level: process.env.EXPO_PUBLIC_LOG_LEVEL as LogLevel,
|
||||
context,
|
||||
contextFilter: process.env.EXPO_PUBLIC_LOG_DEBUG || '',
|
||||
})
|
||||
for (const transport of TRANSPORTS) {
|
||||
logger.addTransport(transport)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
debug(message: string, metadata: Metadata = {}, context?: string) {
|
||||
if (context && !this.debugContextRegexes.find(reg => reg.test(context)))
|
||||
return
|
||||
this.transport(LogLevel.Debug, message, metadata)
|
||||
constructor({
|
||||
level,
|
||||
context,
|
||||
contextFilter,
|
||||
}: {
|
||||
level?: LogLevel
|
||||
context?: LogContext
|
||||
contextFilter?: string
|
||||
} = {}) {
|
||||
this.context = context
|
||||
this.level = level || LogLevel.Info
|
||||
this.contextFilter = contextFilter || ''
|
||||
if (this.contextFilter) {
|
||||
this.level = LogLevel.Debug
|
||||
}
|
||||
this.debugContextRegexes = (this.contextFilter || '')
|
||||
.split(',')
|
||||
.map(filter => {
|
||||
return new RegExp(filter.replace(/[^\w:*-]/, '').replace(/\*/g, '.*'))
|
||||
})
|
||||
}
|
||||
|
||||
debug(message: string, metadata: Metadata = {}) {
|
||||
this.transport({level: LogLevel.Debug, message, metadata})
|
||||
}
|
||||
|
||||
info(message: string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Info, message, metadata)
|
||||
this.transport({level: LogLevel.Info, message, metadata})
|
||||
}
|
||||
|
||||
log(message: string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Log, message, metadata)
|
||||
this.transport({level: LogLevel.Log, message, metadata})
|
||||
}
|
||||
|
||||
warn(message: string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Warn, message, metadata)
|
||||
this.transport({level: LogLevel.Warn, message, metadata})
|
||||
}
|
||||
|
||||
error(error: Error | string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Error, error, metadata)
|
||||
this.transport({level: LogLevel.Error, message: error, metadata})
|
||||
}
|
||||
|
||||
addTransport(transport: Transport) {
|
||||
@@ -217,20 +93,22 @@ export class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.enabled = true
|
||||
}
|
||||
|
||||
protected transport(
|
||||
level: LogLevel,
|
||||
message: string | Error,
|
||||
metadata: Metadata = {},
|
||||
) {
|
||||
if (!this.enabled) return
|
||||
protected transport({
|
||||
level,
|
||||
message,
|
||||
metadata = {},
|
||||
}: {
|
||||
level: LogLevel
|
||||
message: string | Error
|
||||
metadata: Metadata
|
||||
}) {
|
||||
if (
|
||||
level === LogLevel.Debug &&
|
||||
!!this.contextFilter &&
|
||||
!!this.context &&
|
||||
!this.debugContextRegexes.find(reg => reg.test(this.context!))
|
||||
)
|
||||
return
|
||||
|
||||
const timestamp = Date.now()
|
||||
const meta = metadata || {}
|
||||
@@ -240,6 +118,7 @@ export class Logger {
|
||||
id: nanoid(),
|
||||
timestamp,
|
||||
level,
|
||||
context: this.context,
|
||||
message,
|
||||
metadata: meta,
|
||||
})
|
||||
@@ -247,37 +126,20 @@ export class Logger {
|
||||
if (!enabledLogLevels[this.level].includes(level)) return
|
||||
|
||||
for (const transport of this.transports) {
|
||||
transport(level, message, meta, timestamp)
|
||||
transport(level, this.context, message, meta, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger instance. See `@/logger/README` for docs.
|
||||
* Default logger instance. See `@/logger/README` for docs.
|
||||
*
|
||||
* Basic usage:
|
||||
*
|
||||
* `logger.debug(message[, metadata, debugContext])`
|
||||
* `logger.debug(message[, metadata])`
|
||||
* `logger.info(message[, metadata])`
|
||||
* `logger.log(message[, metadata])`
|
||||
* `logger.warn(message[, metadata])`
|
||||
* `logger.error(error[, metadata])`
|
||||
* `logger.disable()`
|
||||
* `logger.enable()`
|
||||
*/
|
||||
export const logger = new Logger()
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
logger.addTransport(createBitdriftTransport())
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (__DEV__) {
|
||||
logger.addTransport(consoleTransport)
|
||||
/*
|
||||
* Comment this out to enable Sentry transport in dev
|
||||
*/
|
||||
// logger.addTransport(sentryTransport)
|
||||
} else {
|
||||
logger.addTransport(sentryTransport)
|
||||
}
|
||||
}
|
||||
export const logger = Logger.create(Logger.Context.Default)
|
||||
|
||||
Reference in New Issue
Block a user