mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add more api logs (#1870)
Adds more detailed API logging using Pino
This commit is contained in:
@ -23,7 +23,6 @@
|
||||
"@documenso/email": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/signing": "*",
|
||||
"@honeybadger-io/js": "^6.10.1",
|
||||
"@lingui/core": "^5.2.0",
|
||||
"@lingui/macro": "^5.2.0",
|
||||
"@lingui/react": "^5.2.0",
|
||||
|
||||
29
packages/lib/types/api-logs.ts
Normal file
29
packages/lib/types/api-logs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { ApiRequestMetadata } from '../universal/extract-request-metadata';
|
||||
|
||||
/**
|
||||
* The minimum required fields that the parent API logger must contain.
|
||||
*/
|
||||
export type RootApiLog = {
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The minimum API log that must be logged at the start of every API request.
|
||||
*/
|
||||
export type BaseApiLog = Partial<RootApiLog> & {
|
||||
path: string;
|
||||
auth: ApiRequestMetadata['auth'];
|
||||
source: ApiRequestMetadata['source'];
|
||||
userId?: number | null;
|
||||
apiTokenId?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The TRPC API log.
|
||||
*/
|
||||
export type TrpcApiLog = BaseApiLog & {
|
||||
trpcMiddleware: string;
|
||||
unverifiedTeamId?: number | null;
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getIpAddress } from './get-ip-address';
|
||||
|
||||
const ZIpSchema = z.string().ip();
|
||||
|
||||
export const ZRequestMetadataSchema = z.object({
|
||||
@ -40,11 +42,13 @@ export type ApiRequestMetadata = {
|
||||
};
|
||||
|
||||
export const extractRequestMetadata = (req: Request): RequestMetadata => {
|
||||
const forwardedFor = req.headers.get('x-forwarded-for');
|
||||
const ip = forwardedFor
|
||||
?.split(',')
|
||||
.map((ip) => ip.trim())
|
||||
.at(0);
|
||||
let ip: string | undefined = undefined;
|
||||
|
||||
try {
|
||||
ip = getIpAddress(req);
|
||||
} catch {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
const parsedIp = ZIpSchema.safeParse(ip);
|
||||
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
import Honeybadger from '@honeybadger-io/js';
|
||||
|
||||
import { env } from './env';
|
||||
|
||||
export const buildLogger = () => {
|
||||
if (env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
|
||||
return new HoneybadgerLogger();
|
||||
}
|
||||
|
||||
return new DefaultLogger();
|
||||
};
|
||||
|
||||
interface LoggerDescriptionOptions {
|
||||
method?: string;
|
||||
path?: string;
|
||||
context?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* The type of log to be captured.
|
||||
*
|
||||
* Defaults to `info`.
|
||||
*/
|
||||
level?: 'info' | 'error' | 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic logger implementation intended to be used in the server side for capturing
|
||||
* explicit errors and other logs.
|
||||
*
|
||||
* Not intended to capture the request and responses.
|
||||
*/
|
||||
interface Logger {
|
||||
log(message: string, options?: LoggerDescriptionOptions): void;
|
||||
|
||||
error(error: Error, options?: LoggerDescriptionOptions): void;
|
||||
}
|
||||
|
||||
class DefaultLogger implements Logger {
|
||||
log(_message: string, _options?: LoggerDescriptionOptions) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
error(_error: Error, _options?: LoggerDescriptionOptions): void {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
class HoneybadgerLogger implements Logger {
|
||||
constructor() {
|
||||
if (!env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY')) {
|
||||
throw new Error('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY is not set');
|
||||
}
|
||||
|
||||
Honeybadger.configure({
|
||||
apiKey: env('NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Honeybadger doesn't really have a non-error logging system.
|
||||
*/
|
||||
log(message: string, options?: LoggerDescriptionOptions) {
|
||||
const { context = {}, level = 'info' } = options || {};
|
||||
|
||||
try {
|
||||
Honeybadger.event({
|
||||
message,
|
||||
context: {
|
||||
level,
|
||||
...context,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
error(error: Error, options?: LoggerDescriptionOptions): void {
|
||||
const { context = {}, level = 'error', method, path } = options || {};
|
||||
|
||||
// const tags = [`level:${level}`];
|
||||
const tags = [];
|
||||
|
||||
let errorMessage = error.message;
|
||||
|
||||
if (method) {
|
||||
tags.push(`method:${method}`);
|
||||
|
||||
errorMessage = `[${method}]: ${error.message}`;
|
||||
}
|
||||
|
||||
if (path) {
|
||||
tags.push(`path:${path}`);
|
||||
}
|
||||
|
||||
try {
|
||||
Honeybadger.notify(errorMessage, {
|
||||
context: {
|
||||
level,
|
||||
...context,
|
||||
},
|
||||
tags,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,27 +1,35 @@
|
||||
import { pino } from 'pino';
|
||||
import { type TransportTargetOptions, pino } from 'pino';
|
||||
|
||||
// const transports: TransportTargetOptions[] = [];
|
||||
import { env } from './env';
|
||||
|
||||
// if (env('NEXT_PRIVATE_LOGGING_DEV')) {
|
||||
// transports.push({
|
||||
// target: 'pino-pretty',
|
||||
// level: 'info',
|
||||
// });
|
||||
// }
|
||||
const transports: TransportTargetOptions[] = [];
|
||||
|
||||
// const loggingFilePath = env('NEXT_PRIVATE_LOGGING_FILE_PATH');
|
||||
if (env('NODE_ENV') !== 'production' && !env('INTERNAL_FORCE_JSON_LOGGER')) {
|
||||
transports.push({
|
||||
target: 'pino-pretty',
|
||||
level: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
// if (loggingFilePath) {
|
||||
// transports.push({
|
||||
// target: 'pino/file',
|
||||
// level: 'info',
|
||||
// options: {
|
||||
// destination: loggingFilePath,
|
||||
// mkdir: true,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
const loggingFilePath = env('NEXT_PRIVATE_LOGGER_FILE_PATH');
|
||||
|
||||
if (loggingFilePath) {
|
||||
transports.push({
|
||||
target: 'pino/file',
|
||||
level: 'info',
|
||||
options: {
|
||||
destination: loggingFilePath,
|
||||
mkdir: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const logger = pino({
|
||||
level: 'info',
|
||||
transport:
|
||||
transports.length > 0
|
||||
? {
|
||||
targets: transports,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user