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:
Tom Moor
2021-09-14 18:04:35 -07:00
committed by GitHub
parent 6c605cf720
commit 83a61b87ed
36 changed files with 508 additions and 264 deletions

117
server/logging/logger.js Normal file
View 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
View 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
View 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;