diff --git a/package.json b/package.json index d58574f3..7abf88fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@charmverse/core", - "version": "0.126.3", + "version": "0.126.4-rc-datadog-logs.1", "description": "Core API for Charmverse", "type": "commonjs", "types": "./dist/cjs/index.d.ts", diff --git a/src/lib/log/logLevel.ts b/src/lib/log/logLevel.ts index fff1f0fa..9629073b 100644 --- a/src/lib/log/logLevel.ts +++ b/src/lib/log/logLevel.ts @@ -1,21 +1,15 @@ import { datadogLogs } from '@datadog/browser-logs'; -import { RateLimit } from 'async-sema'; import type { Logger, LogLevelDesc } from 'loglevel'; import _log from 'loglevel'; import { isNodeEnv, isProdEnv, isStagingEnv } from '../../config/constants'; import { formatLog } from './logUtils'; +import { sendToDatadog } from './sendToDatadog'; -const ERRORS_WEBHOOK = - 'https://discord.com/api/webhooks/898365255703470182/HqS3KqH_7-_dj0KYR6EzNqWhkH0yX6kvV_P32sZ3gnvB8M4AyMoy7W9bbjIul3Hmyu98'; const originalFactory = _log.methodFactory; -const enableDiscordAlerts = isProdEnv && isNodeEnv; const enableDatadogLogs = isProdEnv && !isNodeEnv; -// requests per second = 35, timeUnit = 1sec -const discordRateLimiter = RateLimit(30); - /** * Enable formatting special logs for Datadog in production * Example: @@ -45,17 +39,22 @@ export function apply(log: Logger, logPrefix: string = '') { }); originalMethod.apply(null, args); - // post errors to Discord - if (isProdEnv && methodName === 'error' && enableDiscordAlerts) { - sendErrorToDiscord(ERRORS_WEBHOOK, message, opt).catch((error) => { - logErrorPlain('Error posting to discord', { originalMessage: message, error }); + if (isProdEnv) { + const args2 = formatLog(message, opt, { + formatLogsForDocker: false, + isNodeEnv: false, + logPrefix, + methodName }); + sendToDatadog(methodName, args2[0], args2[1]); } }; }; log.setLevel(log.getLevel()); // Be sure to call setLevel method in order to apply plugin - } else if (enableDatadogLogs) { + } + // send logs to Datadog in production from browser clients + else if (enableDatadogLogs) { log.methodFactory = (methodName, logLevel, loggerName) => { const originalMethod = originalFactory(methodName, logLevel, loggerName); return (message, ...args) => { @@ -82,32 +81,3 @@ function logErrorPlain(message: string, opts: any) { }) ); } - -async function sendErrorToDiscord(webhook: string, message: any, opt: any) { - const fields: { name: string; value?: string }[] = []; - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires - // const http = require('../http'); - // if (opt instanceof Error) { - // fields = [ - // { name: 'Error', value: opt.message }, - // { name: 'Stacktrace', value: opt.stack?.slice(0, 500) } - // ]; - // } else if (opt) { - // fields = Object.entries(opt) - // .map(([name, _value]) => { - // const value = typeof _value === 'string' ? _value.slice(0, 500) : JSON.stringify(_value || {}); - // return { name, value }; - // }) - // .slice(0, 5); // add a sane max # of fields just in case - // } - // await discordRateLimiter(); - // return http.POST(webhook, { - // embeds: [ - // { - // color: 14362664, // #db2828 - // description: message, - // fields - // } - // ] - // }); -} diff --git a/src/lib/log/logUtils.ts b/src/lib/log/logUtils.ts index 4f815a00..658af363 100644 --- a/src/lib/log/logUtils.ts +++ b/src/lib/log/logUtils.ts @@ -2,7 +2,7 @@ import { DateTime } from 'luxon'; export const TIMESTAMP_FORMAT = 'yyyy-LL-dd HH:mm:ss'; -type LogMeta = { +export type LogMeta = { data?: any; error?: { message: string; code?: number; stack?: string }; }; @@ -66,7 +66,7 @@ export function formatTime(date: DateTime) { } // Check if value is primitive value -function _isPrimitiveValue(value: unknown): boolean { +export function _isPrimitiveValue(value: unknown): boolean { return ( typeof value === 'symbol' || typeof value === 'string' || diff --git a/src/lib/log/sendToDatadog.ts b/src/lib/log/sendToDatadog.ts new file mode 100644 index 00000000..20f9ff44 --- /dev/null +++ b/src/lib/log/sendToDatadog.ts @@ -0,0 +1,57 @@ +/* eslint-disable no-console */ +import { _isPrimitiveValue, type LogMeta } from './logUtils'; + +type DatadogLogPayload = { + message: string; + level: string; + timestamp: number; + context?: string; + data?: any; + service: string; + ddsource: string; + ddtags: string; +}; + +const env = process.env.REACT_APP_APP_ENV || process.env.NODE_ENV || 'unknown'; +const service = process.env.DD_SERVICE || 'unknown'; + +const ddtags = `env:${env}`; + +export async function sendToDatadog(level: string, log: string, context?: any) { + if (!process.env.DD_API_KEY) { + return; + } + + let error: LogMeta['error'] = (context as LogMeta | undefined)?.error; + const maybeError = (context as { error?: Error })?.error || context; + if (maybeError instanceof Error) { + error = { ...maybeError, message: maybeError.message, stack: maybeError.stack }; + } + if (_isPrimitiveValue(context) || context instanceof Array) { + context = { data: context }; + } + + const logItem: DatadogLogPayload = { + ddsource: 'nodejs', + ...context, + message: log, + error, + hostName: process.env.HOSTNAME, // defined in beanstalk + service, + ddtags, + level, + timestamp: Date.now() + }; + + const response = await fetch('https://http-intake.logs.datadoghq.com/api/v2/logs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'DD-API-KEY': process.env.DD_API_KEY + }, + body: JSON.stringify(logItem) + }); + if (!response.ok) { + console.error(`Error sending log to Datadog: ${response.status} - ${response.statusText}`); + } +} diff --git a/src/logTest.ts b/src/logTest.ts new file mode 100644 index 00000000..a446e896 --- /dev/null +++ b/src/logTest.ts @@ -0,0 +1,14 @@ +import { log } from './lib/log/log'; + +function main() { + log.info('test'); + log.error('test', { error: new Error('test error') }); + log.warn('test', new Error('test error inline')); + log.debug('test', ['test', 'test2']); + log.trace('test', 'another string'); + setTimeout(() => { + process.exit(0); + }, 1000); +} + +main();