my fork of the bluesky client
at main 340 lines 8.3 kB view raw
1import format from 'date-fns/format' 2import {nanoid} from 'nanoid/non-secure' 3 4import {isNetworkError} from '#/lib/strings/errors' 5import {DebugContext} from '#/logger/debugContext' 6import {add} from '#/logger/logDump' 7import {Sentry} from '#/logger/sentry' 8import * as env from '#/env' 9 10export enum LogLevel { 11 Debug = 'debug', 12 Info = 'info', 13 Log = 'log', 14 Warn = 'warn', 15 Error = 'error', 16} 17 18type Transport = ( 19 level: LogLevel, 20 message: string | Error, 21 metadata: Metadata, 22 timestamp: number, 23) => void 24 25/** 26 * A union of some of Sentry's breadcrumb properties as well as Sentry's 27 * `captureException` parameter, `CaptureContext`. 28 */ 29type Metadata = { 30 /** 31 * Applied as Sentry breadcrumb types. Defaults to `default`. 32 * 33 * @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types 34 */ 35 type?: 36 | 'default' 37 | 'debug' 38 | 'error' 39 | 'navigation' 40 | 'http' 41 | 'info' 42 | 'query' 43 | 'transaction' 44 | 'ui' 45 | 'user' 46 47 /** 48 * Passed through to `Sentry.captureException` 49 * 50 * @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65 51 */ 52 tags?: { 53 [key: string]: 54 | number 55 | string 56 | boolean 57 | bigint 58 | symbol 59 | null 60 | undefined 61 } 62 63 /** 64 * Any additional data, passed through to Sentry as `extra` param on 65 * exceptions, or the `data` param on breadcrumbs. 66 */ 67 [key: string]: unknown 68} & Parameters<typeof Sentry.captureException>[1] 69 70export type ConsoleTransportEntry = { 71 id: string 72 timestamp: number 73 level: LogLevel 74 message: string | Error 75 metadata: Metadata 76} 77 78const enabledLogLevels: { 79 [key in LogLevel]: LogLevel[] 80} = { 81 [LogLevel.Debug]: [ 82 LogLevel.Debug, 83 LogLevel.Info, 84 LogLevel.Log, 85 LogLevel.Warn, 86 LogLevel.Error, 87 ], 88 [LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error], 89 [LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error], 90 [LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error], 91 [LogLevel.Error]: [LogLevel.Error], 92} 93 94export function prepareMetadata(metadata: Metadata): Metadata { 95 return Object.keys(metadata).reduce((acc, key) => { 96 let value = metadata[key] 97 if (value instanceof Error) { 98 value = value.toString() 99 } 100 return {...acc, [key]: value} 101 }, {}) 102} 103 104/** 105 * Used in dev mode to nicely log to the console 106 */ 107export const consoleTransport: Transport = ( 108 level, 109 message, 110 metadata, 111 timestamp, 112) => { 113 const extra = Object.keys(metadata).length 114 ? ' ' + JSON.stringify(prepareMetadata(metadata), null, ' ') 115 : '' 116 const log = { 117 [LogLevel.Debug]: console.debug, 118 [LogLevel.Info]: console.info, 119 [LogLevel.Log]: console.log, 120 [LogLevel.Warn]: console.warn, 121 [LogLevel.Error]: console.error, 122 }[level] 123 124 if (message instanceof Error) { 125 console.info( 126 `${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`, 127 ) 128 log(message) 129 } else { 130 log(`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`) 131 } 132} 133 134export const sentryTransport: Transport = ( 135 level, 136 message, 137 {type, tags, ...metadata}, 138 timestamp, 139) => { 140 const meta = prepareMetadata(metadata) 141 142 /** 143 * If a string, report a breadcrumb 144 */ 145 if (typeof message === 'string') { 146 const severity = ( 147 { 148 [LogLevel.Debug]: 'debug', 149 [LogLevel.Info]: 'info', 150 [LogLevel.Log]: 'log', // Sentry value here is undefined 151 [LogLevel.Warn]: 'warning', 152 [LogLevel.Error]: 'error', 153 } as const 154 )[level] 155 156 Sentry.addBreadcrumb({ 157 message, 158 data: meta, 159 type: type || 'default', 160 level: severity, 161 timestamp: timestamp / 1000, // Sentry expects seconds 162 }) 163 164 // We don't want to send any network errors to sentry 165 if (isNetworkError(message)) { 166 return 167 } 168 169 /** 170 * Send all higher levels with `captureMessage`, with appropriate severity 171 * level 172 */ 173 if (level === 'error' || level === 'warn' || level === 'log') { 174 const messageLevel = ({ 175 [LogLevel.Log]: 'log', 176 [LogLevel.Warn]: 'warning', 177 [LogLevel.Error]: 'error', 178 }[level] || 'log') as Sentry.Breadcrumb['level'] 179 // Defer non-critical messages so they're sent in a batch 180 queueMessageForSentry(message, { 181 level: messageLevel, 182 tags, 183 extra: meta, 184 }) 185 } 186 } else { 187 /** 188 * It's otherwise an Error and should be reported with captureException 189 */ 190 Sentry.captureException(message, { 191 tags, 192 extra: meta, 193 }) 194 } 195} 196 197const queuedMessages: [string, Parameters<typeof Sentry.captureMessage>[1]][] = 198 [] 199let sentrySendTimeout: ReturnType<typeof setTimeout> | null = null 200function queueMessageForSentry( 201 message: string, 202 captureContext: Parameters<typeof Sentry.captureMessage>[1], 203) { 204 queuedMessages.push([message, captureContext]) 205 if (!sentrySendTimeout) { 206 // Throttle sending messages with a leading delay 207 // so that we can get Sentry out of the critical path. 208 sentrySendTimeout = setTimeout(() => { 209 sentrySendTimeout = null 210 sendQueuedMessages() 211 }, 7000) 212 } 213} 214function sendQueuedMessages() { 215 while (queuedMessages.length > 0) { 216 const record = queuedMessages.shift() 217 if (record) { 218 Sentry.captureMessage(record[0], record[1]) 219 } 220 } 221} 222 223/** 224 * Main class. Defaults are provided in the constructor so that subclasses are 225 * technically possible, if we need to go that route in the future. 226 */ 227export class Logger { 228 LogLevel = LogLevel 229 DebugContext = DebugContext 230 231 enabled: boolean 232 level: LogLevel 233 transports: Transport[] = [] 234 235 protected debugContextRegexes: RegExp[] = [] 236 237 constructor({ 238 enabled = !env.IS_TEST, 239 level = env.LOG_LEVEL as LogLevel, 240 debug = env.LOG_DEBUG || '', 241 }: { 242 enabled?: boolean 243 level?: LogLevel 244 debug?: string 245 } = {}) { 246 this.enabled = enabled !== false 247 this.level = debug ? LogLevel.Debug : level ?? LogLevel.Info // default to info 248 this.debugContextRegexes = (debug || '').split(',').map(context => { 249 return new RegExp(context.replace(/[^\w:*]/, '').replace(/\*/g, '.*')) 250 }) 251 } 252 253 debug(message: string, metadata: Metadata = {}, context?: string) { 254 if (context && !this.debugContextRegexes.find(reg => reg.test(context))) 255 return 256 this.transport(LogLevel.Debug, message, metadata) 257 } 258 259 info(message: string, metadata: Metadata = {}) { 260 this.transport(LogLevel.Info, message, metadata) 261 } 262 263 log(message: string, metadata: Metadata = {}) { 264 this.transport(LogLevel.Log, message, metadata) 265 } 266 267 warn(message: string, metadata: Metadata = {}) { 268 this.transport(LogLevel.Warn, message, metadata) 269 } 270 271 error(error: Error | string, metadata: Metadata = {}) { 272 this.transport(LogLevel.Error, error, metadata) 273 } 274 275 addTransport(transport: Transport) { 276 this.transports.push(transport) 277 return () => { 278 this.transports.splice(this.transports.indexOf(transport), 1) 279 } 280 } 281 282 disable() { 283 this.enabled = false 284 } 285 286 enable() { 287 this.enabled = true 288 } 289 290 protected transport( 291 level: LogLevel, 292 message: string | Error, 293 metadata: Metadata = {}, 294 ) { 295 if (!this.enabled) return 296 297 const timestamp = Date.now() 298 const meta = metadata || {} 299 300 // send every log to syslog 301 add({ 302 id: nanoid(), 303 timestamp, 304 level, 305 message, 306 metadata: meta, 307 }) 308 309 if (!enabledLogLevels[this.level].includes(level)) return 310 311 for (const transport of this.transports) { 312 transport(level, message, meta, timestamp) 313 } 314 } 315} 316 317/** 318 * Logger instance. See `@/logger/README` for docs. 319 * 320 * Basic usage: 321 * 322 * `logger.debug(message[, metadata, debugContext])` 323 * `logger.info(message[, metadata])` 324 * `logger.warn(message[, metadata])` 325 * `logger.error(error[, metadata])` 326 * `logger.disable()` 327 * `logger.enable()` 328 */ 329export const logger = new Logger() 330 331if (env.IS_DEV && !env.IS_TEST) { 332 logger.addTransport(consoleTransport) 333 334 /* 335 * Comment this out to disable Sentry transport in dev 336 */ 337 // logger.addTransport(sentryTransport) 338} else if (env.IS_PROD) { 339 logger.addTransport(sentryTransport) 340}