feat: Normalized server logging (#2567)
* feat: Normalize logging * Remove scattered console.error + Sentry.captureException * Remove mention of debug * cleanup dev output * Edge cases, docs * Refactor: Move logger, metrics, sentry under 'logging' folder. Trying to reduce the amount of things under generic 'utils' * cleanup, last few console calls
This commit is contained in:
117
server/logging/logger.js
Normal file
117
server/logging/logger.js
Normal file
@ -0,0 +1,117 @@
|
||||
// @flow
|
||||
import chalk from "chalk";
|
||||
import winston from "winston";
|
||||
import env from "../env";
|
||||
import Metrics from "../logging/metrics";
|
||||
import Sentry from "../logging/sentry";
|
||||
|
||||
const isProduction = env.NODE_ENV === "production";
|
||||
|
||||
type LogCategory =
|
||||
| "lifecycle"
|
||||
| "collaboration"
|
||||
| "http"
|
||||
| "commands"
|
||||
| "processor"
|
||||
| "email"
|
||||
| "queue"
|
||||
| "database"
|
||||
| "utils";
|
||||
|
||||
type Extra = { [key: string]: any };
|
||||
|
||||
class Logger {
|
||||
output: any;
|
||||
|
||||
constructor() {
|
||||
this.output = winston.createLogger();
|
||||
this.output.add(
|
||||
new winston.transports.Console({
|
||||
format: isProduction
|
||||
? winston.format.json()
|
||||
: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(
|
||||
({ message, label }) =>
|
||||
`${label ? chalk.bold("[" + label + "] ") : ""}${message}`
|
||||
)
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log information
|
||||
*
|
||||
* @param category A log message category that will be prepended
|
||||
* @param extra Arbitrary data to be logged that will appear in prod logs
|
||||
*/
|
||||
info(label: LogCategory, message: string, extra?: Extra) {
|
||||
this.output.info(message, { ...extra, label });
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug information
|
||||
*
|
||||
* @param category A log message category that will be prepended
|
||||
* @param extra Arbitrary data to be logged that will appear in prod logs
|
||||
*/
|
||||
debug(label: LogCategory, message: string, extra?: Extra) {
|
||||
this.output.debug(message, { ...extra, label });
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning
|
||||
*
|
||||
* @param message A warning message
|
||||
* @param extra Arbitrary data to be logged that will appear in prod logs
|
||||
*/
|
||||
warn(message: string, extra?: Extra) {
|
||||
Metrics.increment("logger.warning");
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.withScope(function (scope) {
|
||||
for (const key in extra) {
|
||||
scope.setExtra(key, extra[key]);
|
||||
scope.setLevel(Sentry.Severity.Warning);
|
||||
}
|
||||
Sentry.captureMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
if (isProduction) {
|
||||
this.output.warn(message, extra);
|
||||
} else {
|
||||
console.warn(message, extra);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a runtime error
|
||||
*
|
||||
* @param message A description of the error
|
||||
* @param error The error that occurred
|
||||
* @param extra Arbitrary data to be logged that will appear in prod logs
|
||||
*/
|
||||
error(message: string, error: Error, extra?: Extra) {
|
||||
Metrics.increment("logger.error");
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.withScope(function (scope) {
|
||||
for (const key in extra) {
|
||||
scope.setExtra(key, extra[key]);
|
||||
scope.setLevel(Sentry.Severity.Error);
|
||||
}
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
}
|
||||
|
||||
if (isProduction) {
|
||||
this.output.error(message, { error: error.message, stack: error.stack });
|
||||
} else {
|
||||
console.error(message, { error, extra });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Logger();
|
||||
51
server/logging/metrics.js
Normal file
51
server/logging/metrics.js
Normal file
@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
import ddMetrics from "datadog-metrics";
|
||||
|
||||
class Metrics {
|
||||
enabled: boolean = !!process.env.DD_API_KEY;
|
||||
|
||||
constructor() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ddMetrics.init({
|
||||
apiKey: process.env.DD_API_KEY,
|
||||
prefix: "outline.",
|
||||
defaultTags: [`env:${process.env.DD_ENV || process.env.NODE_ENV}`],
|
||||
});
|
||||
}
|
||||
|
||||
gauge(key: string, value: number, tags?: string[]): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ddMetrics.gauge(key, value, tags);
|
||||
}
|
||||
|
||||
gaugePerInstance(key: string, value: number, tags?: string[] = []): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instanceId = process.env.INSTANCE_ID || process.env.HEROKU_DYNO_ID;
|
||||
if (!instanceId) {
|
||||
throw new Error(
|
||||
"INSTANCE_ID or HEROKU_DYNO_ID must be set when using DataDog"
|
||||
);
|
||||
}
|
||||
|
||||
return ddMetrics.gauge(key, value, [...tags, `instance:${instanceId}`]);
|
||||
}
|
||||
|
||||
increment(key: string, tags?: { [string]: string }): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ddMetrics.increment(key, tags);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Metrics();
|
||||
57
server/logging/sentry.js
Normal file
57
server/logging/sentry.js
Normal file
@ -0,0 +1,57 @@
|
||||
// @flow
|
||||
import * as Sentry from "@sentry/node";
|
||||
import env from "../env";
|
||||
import type { ContextWithState } from "../types";
|
||||
|
||||
if (env.SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: env.SENTRY_DSN,
|
||||
environment: env.ENVIRONMENT,
|
||||
release: env.RELEASE,
|
||||
maxBreadcrumbs: 0,
|
||||
ignoreErrors: [
|
||||
// emitted by Koa when bots attempt to snoop on paths such as wp-admin
|
||||
// or the user client submits a bad request. These are expected in normal
|
||||
// running of the application and don't need to be reported.
|
||||
"BadRequestError",
|
||||
"UnauthorizedError",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function requestErrorHandler(error: any, ctx: ContextWithState) {
|
||||
// we don't need to report every time a request stops to the bug tracker
|
||||
if (error.code === "EPIPE" || error.code === "ECONNRESET") {
|
||||
console.warn("Connection error", { error });
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.withScope(function (scope) {
|
||||
const requestId = ctx.headers["x-request-id"];
|
||||
if (requestId) {
|
||||
scope.setTag("request_id", requestId);
|
||||
}
|
||||
|
||||
const authType = ctx.state ? ctx.state.authType : undefined;
|
||||
if (authType) {
|
||||
scope.setTag("auth_type", authType);
|
||||
}
|
||||
|
||||
const userId =
|
||||
ctx.state && ctx.state.user ? ctx.state.user.id : undefined;
|
||||
if (userId) {
|
||||
scope.setUser({ id: userId });
|
||||
}
|
||||
|
||||
scope.addEventProcessor(function (event) {
|
||||
return Sentry.Handlers.parseRequest(event, ctx.request);
|
||||
});
|
||||
Sentry.captureException(error);
|
||||
});
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export default Sentry;
|
||||
Reference in New Issue
Block a user