Add new logger
This commit is contained in:
parent
445f976881
commit
fd93bf2146
13 changed files with 888 additions and 18 deletions
99
src/logger/README.md
Normal file
99
src/logger/README.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
# Logger
|
||||
|
||||
Simple logger for Bluesky. Supports log levels, debug contexts, and separate
|
||||
transports for production, dev, and test mode.
|
||||
|
||||
## At a Glance
|
||||
|
||||
```typescript
|
||||
import { logger } from '#/logger'
|
||||
|
||||
logger.debug(message[, metadata, debugContext])
|
||||
logger.info(message[, metadata])
|
||||
logger.log(message[, metadata])
|
||||
logger.warn(message[, metadata])
|
||||
logger.error(error[, metadata])
|
||||
```
|
||||
|
||||
#### Modes
|
||||
|
||||
The "modes" referred to here are inferred from the values exported from `#/env`.
|
||||
Basically, the booleans `IS_DEV`, `IS_TEST`, and `IS_PROD`.
|
||||
|
||||
#### Log Levels
|
||||
|
||||
Log levels are used to filter which logs are either printed to the console
|
||||
and/or sent to Sentry and other reporting services. To configure, set the
|
||||
`EXPO_PUBLIC_LOG_LEVEL` environment variable in `.env` to one of `debug`,
|
||||
`info`, `log`, `warn`, or `error`.
|
||||
|
||||
This variable should be `info` in production, and `debug` in dev. If it gets too
|
||||
noisy in dev, simply set it to a higher level, such as `warn`.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { logger } from '#/logger';
|
||||
```
|
||||
|
||||
### `logger.error`
|
||||
|
||||
The `error` level is for... well, errors. These are sent to Sentry in production mode.
|
||||
|
||||
`error`, along with all log levels, supports an additional parameter, `metadata: Record<string, unknown>`. Use this to provide values to the [Sentry
|
||||
breadcrumb](https://docs.sentry.io/platforms/react-native/enriching-events/breadcrumbs/#manual-breadcrumbs).
|
||||
|
||||
```typescript
|
||||
try {
|
||||
// some async code
|
||||
} catch (e) {
|
||||
logger.error(e, { ...metadata });
|
||||
}
|
||||
```
|
||||
|
||||
### `logger.warn`
|
||||
|
||||
Warnings will be sent to Sentry as a separate Issue with level `warning`, as
|
||||
well as as breadcrumbs, with a severity level of `warning`
|
||||
|
||||
### `logger.log`
|
||||
|
||||
Logs with level `log` will be sent to Sentry as a separate Issue with level `log`, as
|
||||
well as as breadcrumbs, with a severity level of `default`.
|
||||
|
||||
### `logger.info`
|
||||
|
||||
The `info` level should be used for information that would be helpful in a
|
||||
tracing context, like Sentry. In production mode, `info` logs are sent
|
||||
to Sentry as breadcrumbs, which decorate log levels above `info` such as `log`,
|
||||
`warn`, and `error`.
|
||||
|
||||
### `logger.debug`
|
||||
|
||||
Debug level is really only intended for local development. Use this instead of
|
||||
`console.log`.
|
||||
|
||||
```typescript
|
||||
logger.debug(message, { ...metadata });
|
||||
```
|
||||
|
||||
Inspired by [debug](https://www.npmjs.com/package/debug), when writing debug
|
||||
logs, you can optionally pass a _context_, which can be then filtered when in
|
||||
debug mode.
|
||||
|
||||
This value should be related to the feature, component, or screen
|
||||
the code is running within, and **it should be defined in `#/logger/debugContext`**.
|
||||
This way we know if a relevant context already exists, and we can trace all
|
||||
active contexts in use in our app. This const enum is conveniently available on
|
||||
the `logger` at `logger.DebugContext`.
|
||||
|
||||
For example, a debug log like this:
|
||||
|
||||
```typescript
|
||||
logger.debug(message, {}, logger.DebugContext.composer);
|
||||
```
|
||||
|
||||
Would be logged to the console in dev mode if `EXPO_PUBLIC_LOG_LEVEL=debug`, _or_ if you
|
||||
pass a separate environment variable `LOG_DEBUG=composer`. This variable supports
|
||||
multiple contexts using commas like `LOG_DEBUG=composer,profile`, and _automatically
|
||||
sets the log level to `debug`, regardless of `EXPO_PUBLIC_LOG_LEVEL`._
|
424
src/logger/__tests__/logger.test.ts
Normal file
424
src/logger/__tests__/logger.test.ts
Normal file
|
@ -0,0 +1,424 @@
|
|||
import {nanoid} from 'nanoid/non-secure'
|
||||
import {jest, describe, expect, test, beforeAll} from '@jest/globals'
|
||||
import {Native as Sentry} from 'sentry-expo'
|
||||
|
||||
import {Logger, LogLevel, sentryTransport} from '#/logger'
|
||||
|
||||
jest.mock('#/env', () => ({
|
||||
IS_TEST: true,
|
||||
IS_DEV: false,
|
||||
IS_PROD: false,
|
||||
/*
|
||||
* Forces debug mode for tests using the default logger. Most tests create
|
||||
* their own logger instance.
|
||||
*/
|
||||
LOG_LEVEL: 'debug',
|
||||
LOG_DEBUG: '',
|
||||
}))
|
||||
|
||||
jest.mock('sentry-expo', () => ({
|
||||
Native: {
|
||||
addBreadcrumb: jest.fn(),
|
||||
captureException: jest.fn(),
|
||||
captureMessage: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
describe('general functionality', () => {
|
||||
test('default params', () => {
|
||||
const logger = new Logger()
|
||||
expect(logger.enabled).toBeFalsy()
|
||||
expect(logger.level).toEqual(LogLevel.Debug) // mocked above
|
||||
})
|
||||
|
||||
test('can override default params', () => {
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
level: LogLevel.Info,
|
||||
})
|
||||
expect(logger.enabled).toBeTruthy()
|
||||
expect(logger.level).toEqual(LogLevel.Info)
|
||||
})
|
||||
|
||||
test('disabled logger does not report', () => {
|
||||
const logger = new Logger({
|
||||
enabled: false,
|
||||
level: LogLevel.Debug,
|
||||
})
|
||||
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug('message')
|
||||
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('disablement', () => {
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
level: LogLevel.Debug,
|
||||
})
|
||||
|
||||
logger.disable()
|
||||
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug('message')
|
||||
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('passing debug contexts automatically enables debug mode', () => {
|
||||
const logger = new Logger({debug: 'specific'})
|
||||
expect(logger.level).toEqual(LogLevel.Debug)
|
||||
})
|
||||
|
||||
test('supports extra metadata', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({enabled: true})
|
||||
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
const extra = {foo: true}
|
||||
logger.warn('message', extra)
|
||||
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
'message',
|
||||
extra,
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('supports nullish/falsy metadata', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({enabled: true})
|
||||
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
const remove = logger.addTransport(mockTransport)
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('a', null)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
'a',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('b', false)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
'b',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('c', 0)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
'c',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
remove()
|
||||
|
||||
logger.addTransport((level, message, metadata) => {
|
||||
expect(typeof metadata).toEqual('object')
|
||||
})
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('message', null)
|
||||
})
|
||||
|
||||
test('sentryTransport', () => {
|
||||
const message = 'message'
|
||||
const timestamp = Date.now()
|
||||
const sentryTimestamp = timestamp / 1000
|
||||
|
||||
sentryTransport(LogLevel.Debug, message, {}, timestamp)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
message,
|
||||
data: {},
|
||||
type: 'default',
|
||||
level: LogLevel.Debug,
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Info,
|
||||
message,
|
||||
{type: 'info', prop: true},
|
||||
timestamp,
|
||||
)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
message,
|
||||
data: {prop: true},
|
||||
type: 'info',
|
||||
level: LogLevel.Info,
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
|
||||
sentryTransport(LogLevel.Log, message, {}, timestamp)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
message,
|
||||
data: {},
|
||||
type: 'default',
|
||||
level: 'debug', // Sentry bug, log becomes debug
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
||||
level: 'log',
|
||||
tags: undefined,
|
||||
extra: {},
|
||||
})
|
||||
|
||||
sentryTransport(LogLevel.Warn, message, {}, timestamp)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
message,
|
||||
data: {},
|
||||
type: 'default',
|
||||
level: 'warning',
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
||||
level: 'warning',
|
||||
tags: undefined,
|
||||
extra: {},
|
||||
})
|
||||
|
||||
const e = new Error('error')
|
||||
const tags = {
|
||||
prop: 'prop',
|
||||
}
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Error,
|
||||
e,
|
||||
{
|
||||
tags,
|
||||
prop: true,
|
||||
},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(e, {
|
||||
tags,
|
||||
extra: {
|
||||
prop: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('add/remove transport', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({enabled: true})
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
const remove = logger.addTransport(mockTransport)
|
||||
|
||||
logger.warn('warn')
|
||||
|
||||
remove()
|
||||
|
||||
logger.warn('warn')
|
||||
|
||||
// only called once bc it was removed
|
||||
expect(mockTransport).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
LogLevel.Warn,
|
||||
'warn',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('debug contexts', () => {
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
test('specific', () => {
|
||||
const timestamp = Date.now()
|
||||
const message = nanoid()
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
debug: 'specific',
|
||||
})
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug(message, {}, 'specific')
|
||||
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('namespaced', () => {
|
||||
const timestamp = Date.now()
|
||||
const message = nanoid()
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
debug: 'namespace*',
|
||||
})
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug(message, {}, 'namespace')
|
||||
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('ignores inactive', () => {
|
||||
const timestamp = Date.now()
|
||||
const message = nanoid()
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
debug: 'namespace:foo:*',
|
||||
})
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug(message, {}, 'namespace:bar:baz')
|
||||
|
||||
expect(mockTransport).not.toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('supports levels', () => {
|
||||
test('debug', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
level: LogLevel.Debug,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Info,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
logger.warn(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
const e = new Error(message)
|
||||
logger.error(e)
|
||||
expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp)
|
||||
})
|
||||
|
||||
test('info', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
level: LogLevel.Info,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Info,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('warn', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
level: LogLevel.Warn,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.warn(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('error', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
enabled: true,
|
||||
level: LogLevel.Error,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.warn(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
const e = new Error('original message')
|
||||
logger.error(e)
|
||||
expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp)
|
||||
})
|
||||
})
|
10
src/logger/debugContext.ts
Normal file
10
src/logger/debugContext.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* *Do not import this directly.* Instead, use the shortcut reference `logger.DebugContext`.
|
||||
*
|
||||
* Add debug contexts here. Although convention typically calls for enums ito
|
||||
* be capitalized, for parity with the `LOG_DEBUG` env var, please use all
|
||||
* lowercase.
|
||||
*/
|
||||
export const DebugContext = {
|
||||
// e.g. composer: 'composer'
|
||||
} as const
|
290
src/logger/index.ts
Normal file
290
src/logger/index.ts
Normal file
|
@ -0,0 +1,290 @@
|
|||
import format from 'date-fns/format'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {Sentry} from '#/logger/sentry'
|
||||
import * as env from '#/env'
|
||||
import {DebugContext} from '#/logger/debugContext'
|
||||
import {add} from '#/logger/logDump'
|
||||
|
||||
export enum LogLevel {
|
||||
Debug = 'debug',
|
||||
Info = 'info',
|
||||
Log = 'log',
|
||||
Warn = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
type Transport = (
|
||||
level: LogLevel,
|
||||
message: string | Error,
|
||||
metadata: Metadata,
|
||||
timestamp: number,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* A union of some of Sentry's breadcrumb properties as well as Sentry's
|
||||
* `captureException` parameter, `CaptureContext`.
|
||||
*/
|
||||
type Metadata = {
|
||||
/**
|
||||
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
||||
*
|
||||
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
|
||||
*/
|
||||
type?:
|
||||
| 'default'
|
||||
| 'debug'
|
||||
| 'error'
|
||||
| 'navigation'
|
||||
| 'http'
|
||||
| 'info'
|
||||
| 'query'
|
||||
| 'transaction'
|
||||
| 'ui'
|
||||
| 'user'
|
||||
|
||||
/**
|
||||
* Passed through to `Sentry.captureException`
|
||||
*
|
||||
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
|
||||
*/
|
||||
tags?: {
|
||||
[key: string]:
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| bigint
|
||||
| symbol
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Any additional data, passed through to Sentry as `extra` param on
|
||||
* exceptions, or the `data` param on breadcrumbs.
|
||||
*/
|
||||
[key: string]: unknown
|
||||
} & Parameters<typeof Sentry.captureException>[1]
|
||||
|
||||
export type ConsoleTransportEntry = {
|
||||
id: string
|
||||
timestamp: number
|
||||
level: LogLevel
|
||||
message: string | Error
|
||||
metadata: Metadata
|
||||
}
|
||||
|
||||
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],
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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]
|
||||
|
||||
log(`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`)
|
||||
}
|
||||
|
||||
export const sentryTransport: Transport = (
|
||||
level,
|
||||
message,
|
||||
{type, tags, ...metadata},
|
||||
timestamp,
|
||||
) => {
|
||||
/**
|
||||
* 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: metadata,
|
||||
type: type || 'default',
|
||||
level: severity,
|
||||
timestamp: timestamp / 1000, // Sentry expects seconds
|
||||
})
|
||||
|
||||
/**
|
||||
* 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']
|
||||
|
||||
Sentry.captureMessage(message, {
|
||||
level: messageLevel,
|
||||
tags,
|
||||
extra: metadata,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* It's otherwise an Error and should be reported with captureException
|
||||
*/
|
||||
Sentry.captureException(message, {
|
||||
tags,
|
||||
extra: metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
enabled: boolean
|
||||
level: LogLevel
|
||||
transports: Transport[] = []
|
||||
|
||||
protected debugContextRegexes: RegExp[] = []
|
||||
|
||||
constructor({
|
||||
enabled = !env.IS_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, '.*'))
|
||||
})
|
||||
}
|
||||
|
||||
debug(message: string, metadata: Metadata = {}, context?: string) {
|
||||
if (context && !this.debugContextRegexes.find(reg => reg.test(context)))
|
||||
return
|
||||
this.transport(LogLevel.Debug, message, metadata)
|
||||
}
|
||||
|
||||
info(message: string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Info, message, metadata)
|
||||
}
|
||||
|
||||
log(message: string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Log, message, metadata)
|
||||
}
|
||||
|
||||
warn(message: string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Warn, message, metadata)
|
||||
}
|
||||
|
||||
error(error: Error | string, metadata: Metadata = {}) {
|
||||
this.transport(LogLevel.Error, error, metadata)
|
||||
}
|
||||
|
||||
addTransport(transport: Transport) {
|
||||
this.transports.push(transport)
|
||||
return () => {
|
||||
this.transports.splice(this.transports.indexOf(transport), 1)
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.enabled = true
|
||||
}
|
||||
|
||||
protected transport(
|
||||
level: LogLevel,
|
||||
message: string | Error,
|
||||
metadata: Metadata = {},
|
||||
) {
|
||||
if (!this.enabled) return
|
||||
if (!enabledLogLevels[this.level].includes(level)) return
|
||||
|
||||
const timestamp = Date.now()
|
||||
const meta = metadata || {}
|
||||
|
||||
for (const transport of this.transports) {
|
||||
transport(level, message, meta, timestamp)
|
||||
}
|
||||
|
||||
add({
|
||||
id: nanoid(),
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
metadata: meta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger instance. See `@/logger/README` for docs.
|
||||
*
|
||||
* Basic usage:
|
||||
*
|
||||
* `logger.debug(message[, metadata, debugContext])`
|
||||
* `logger.info(message[, metadata])`
|
||||
* `logger.warn(message[, metadata])`
|
||||
* `logger.error(error[, metadata])`
|
||||
* `logger.disable()`
|
||||
* `logger.enable()`
|
||||
*/
|
||||
export const logger = new Logger()
|
||||
|
||||
/**
|
||||
* Report to console in dev, Sentry in prod, nothing in test.
|
||||
*/
|
||||
if (env.IS_DEV && !env.IS_TEST) {
|
||||
logger.addTransport(consoleTransport)
|
||||
|
||||
/**
|
||||
* Uncomment this to test Sentry in dev
|
||||
*/
|
||||
// logger.addTransport(sentryTransport);
|
||||
} else if (env.IS_PROD) {
|
||||
logger.addTransport(sentryTransport)
|
||||
}
|
12
src/logger/logDump.ts
Normal file
12
src/logger/logDump.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {ConsoleTransportEntry} from '#/logger'
|
||||
|
||||
let entries: ConsoleTransportEntry[] = []
|
||||
|
||||
export function add(entry: ConsoleTransportEntry) {
|
||||
entries.unshift(entry)
|
||||
entries = entries.slice(0, 50)
|
||||
}
|
||||
|
||||
export function getEntries() {
|
||||
return entries
|
||||
}
|
1
src/logger/sentry/index.ts
Normal file
1
src/logger/sentry/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {Native as Sentry} from 'sentry-expo'
|
1
src/logger/sentry/index.web.ts
Normal file
1
src/logger/sentry/index.web.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {Browser as Sentry} from 'sentry-expo'
|
Loading…
Add table
Add a link
Reference in a new issue