From 83a61b87ed59c4c61dda20a7fe71f37e8aa7faa6 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Tue, 14 Sep 2021 18:04:35 -0700 Subject: [PATCH] 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 --- .env.sample | 6 +- README.md | 6 +- app/index.js | 2 +- flow-typed/globals.js | 2 + package.json | 6 +- server/collaboration/logger.js | 15 +- server/collaboration/persistence.js | 15 +- server/collaboration/tracing.js | 20 +-- server/commands/collectionImporter.js | 6 +- server/commands/documentPermanentDeleter.js | 8 +- server/commands/teamCreator.js | 9 +- server/index.js | 16 +- server/logging/logger.js | 117 ++++++++++++++ server/logging/metrics.js | 51 ++++++ server/{utils => logging}/sentry.js | 0 server/mailer.js | 25 +-- server/middlewares/passport.js | 8 +- server/models/Team.js | 4 +- server/models/User.js | 6 +- server/queues/processors/exports.js | 25 ++- server/queues/processors/notifications.js | 7 +- server/redis.js | 3 +- server/routes/api/utils.js | 15 +- server/routes/auth/index.js | 3 - .../20210226232041-migrate-authentication.js | 18 ++- server/sequelize.js | 4 +- server/services/web.js | 5 +- server/services/websockets.js | 47 ++---- server/services/worker.js | 40 ++--- server/utils/authentication.js | 6 +- server/utils/metrics.js | 45 ------ server/utils/queue.js | 20 +-- server/utils/s3.js | 24 ++- server/utils/startup.js | 31 ++-- server/utils/zip.js | 10 +- yarn.lock | 147 +++++++++++++++++- 36 files changed, 508 insertions(+), 264 deletions(-) create mode 100644 server/logging/logger.js create mode 100644 server/logging/metrics.js rename server/{utils => logging}/sentry.js (100%) delete mode 100644 server/utils/metrics.js diff --git a/.env.sample b/.env.sample index 640e03fc..a2503804 100644 --- a/.env.sample +++ b/.env.sample @@ -120,9 +120,9 @@ WEB_CONCURRENCY=1 # especially large Word documents with embedded imagery MAXIMUM_IMPORT_SIZE=5120000 -# You may enable or disable debugging categories to increase the noisiness of -# logs. The default is a good balance -DEBUG=cache,presenters,events,emails,mailer,utils,http,server,processors +# You can remove this line if your reverse proxy already logs incoming http +# requests and this ends up being duplicative +DEBUG=http # Comma separated list of domains to be allowed to signin to the wiki. If not # set, all domains are allowed by default when using Google OAuth to signin diff --git a/README.md b/README.md index 9a9b2112..1a8b09ba 100644 --- a/README.md +++ b/README.md @@ -122,11 +122,9 @@ please refer to the [architecture document](docs/ARCHITECTURE.md) first for a hi ## Debugging -Outline uses [debug](https://www.npmjs.com/package/debug). To enable debugging output, the following categories are available: +In development Outline outputs simple logging to the console, prefixed by categories. In production it outputs JSON logs, these can be easily parsed by your preferred log ingestion pipeline. -``` -DEBUG=sql,cache,presenters,events,importer,exporter,emails,mailer -``` +HTTP logging is disabled by default, but can be enabled by setting the `DEBUG=http` environment variable. ## Tests diff --git a/app/index.js b/app/index.js index 36a2f629..608a8fe6 100644 --- a/app/index.js +++ b/app/index.js @@ -15,8 +15,8 @@ import ScrollToTop from "components/ScrollToTop"; import Theme from "components/Theme"; import Toasts from "components/Toasts"; import Routes from "./routes"; +import { initSentry } from "./utils/sentry"; import env from "env"; -import { initSentry } from "utils/sentry"; initI18n(); diff --git a/flow-typed/globals.js b/flow-typed/globals.js index 0d22cde7..94c5871b 100644 --- a/flow-typed/globals.js +++ b/flow-typed/globals.js @@ -9,6 +9,8 @@ declare var process: { env: { [string]: string, }, + stdout: Stream, + stderr: Stream, }; declare var EDITOR_VERSION: string; diff --git a/package.json b/package.json index 0260c0ab..9dea696d 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ "scripts": { "clean": "rimraf build", "build:i18n": "i18next --silent 'app/**/*.js' 'server/**/*.js' && mkdir -p ./build/shared/i18n && cp -R ./shared/i18n/locales ./build/shared/i18n", - "build:server": "babel -d ./build/server ./server && babel -d ./build/shared ./shared && cp ./server/collaboration/Procfile ./build/server/collaboration/Procfile && cp package.json ./build && ln -sf \"$(pwd)/webpack.config.dev.js\" ./build", + "build:server": "babel --quiet -d ./build/server ./server && babel --quiet -d./build/shared ./shared && cp ./server/collaboration/Procfile ./build/server/collaboration/Procfile && cp package.json ./build && ln -sf \"$(pwd)/webpack.config.dev.js\" ./build", "build:webpack": "webpack --config webpack.config.prod.js", "build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server", "start": "node ./build/server/index.js", - "dev": "yarn concurrently --kill-others -n server,multiplayer \"node --inspect=0.0.0.0 build/server/index.js --services=websockets,admin,web,worker\" \"node build/server/index.js --services=collaboration --port=4000\"", + "dev": "yarn concurrently --kill-others -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=websockets,admin,web,worker\" \"node build/server/index.js --services=collaboration --port=4000\"", "dev:watch": "nodemon --exec \"yarn build:server && yarn build:i18n && yarn dev\" -e js --ignore build/ --ignore app/ --ignore flow-typed/", "lint": "eslint app server shared", "deploy": "git push heroku master", @@ -73,7 +73,6 @@ "datadog-metrics": "^0.9.3", "date-fns": "2.22.1", "dd-trace": "^0.32.2", - "debug": "^4.1.1", "dotenv": "^4.0.0", "emoji-regex": "^6.5.1", "es6-error": "^4.1.1", @@ -175,6 +174,7 @@ "utf8": "^3.0.0", "uuid": "^8.3.2", "validator": "5.2.0", + "winston": "^3.3.3", "y-indexeddb": "^9.0.6", "y-prosemirror": "^1.0.9", "yjs": "^13.5.12" diff --git a/server/collaboration/logger.js b/server/collaboration/logger.js index 570a2e0f..3179f3e0 100644 --- a/server/collaboration/logger.js +++ b/server/collaboration/logger.js @@ -1,22 +1,19 @@ // @flow -import debug from "debug"; - -const log = debug("server"); - -export default class Logger { +import Logger from "../logging/logger"; +export default class CollaborationLogger { async onCreateDocument(data: { documentName: string }) { - log(`Created document "${data.documentName}"`); + Logger.info("collaboration", `Created document "${data.documentName}"`); } async onConnect(data: { documentName: string }) { - log(`New connection to "${data.documentName}"`); + Logger.info("collaboration", `New connection to "${data.documentName}"`); } async onDisconnect(data: { documentName: string }) { - log(`Connection to "${data.documentName}" closed`); + Logger.info("collaboration", `Connection to "${data.documentName}" closed`); } async onUpgrade() { - log("Upgrading connection"); + Logger.info("collaboration", "Upgrading connection"); } } diff --git a/server/collaboration/persistence.js b/server/collaboration/persistence.js index 3d361f37..65628056 100644 --- a/server/collaboration/persistence.js +++ b/server/collaboration/persistence.js @@ -1,12 +1,11 @@ // @flow -import debug from "debug"; import { debounce } from "lodash"; import * as Y from "yjs"; import documentUpdater from "../commands/documentUpdater"; +import Logger from "../logging/logger"; import { Document, User } from "../models"; import markdownToYDoc from "./utils/markdownToYDoc"; -const log = debug("server"); const DELAY = 3000; export default class Persistence { @@ -30,12 +29,18 @@ export default class Persistence { if (document.state) { const ydoc = new Y.Doc(); - log(`Document ${documentId} is already in state`); + Logger.info( + "collaboration", + `Document ${documentId} is in database state` + ); Y.applyUpdate(ydoc, document.state); return ydoc; } - log(`Document ${documentId} is not in state, creating state from markdown`); + Logger.info( + "collaboration", + `Document ${documentId} is not in state, creating from markdown` + ); const ydoc = markdownToYDoc(document.text, fieldName); const state = Y.encodeStateAsUpdate(ydoc); @@ -55,7 +60,7 @@ export default class Persistence { }) => { const [, documentId] = documentName.split("."); - log(`persisting ${documentId}`); + Logger.info("collaboration", `Persisting ${documentId}`); await documentUpdater({ documentId, diff --git a/server/collaboration/tracing.js b/server/collaboration/tracing.js index 9a3552a0..7b3e4164 100644 --- a/server/collaboration/tracing.js +++ b/server/collaboration/tracing.js @@ -1,40 +1,40 @@ // @flow -import * as metrics from "../utils/metrics"; +import Metrics from "../logging/metrics"; let count = 0; export default class Tracing { async onCreateDocument({ documentName }: { documentName: string }) { - metrics.increment("collaboration.create_document", { documentName }); + Metrics.increment("collaboration.create_document", { documentName }); // TODO: Waiting for `instance` available in payload - // metrics.gaugePerInstance( + // Metrics.gaugePerInstance( // "collaboration.documents_count", // instance.documents.size() // ); } async onAuthenticationFailed({ documentName }: { documentName: string }) { - metrics.increment("collaboration.authentication_failed", { documentName }); + Metrics.increment("collaboration.authentication_failed", { documentName }); } async onConnect({ documentName }: { documentName: string }) { - metrics.increment("collaboration.connect", { documentName }); - metrics.gaugePerInstance("collaboration.connections_count", ++count); + Metrics.increment("collaboration.connect", { documentName }); + Metrics.gaugePerInstance("collaboration.connections_count", ++count); } async onDisconnect({ documentName }: { documentName: string }) { - metrics.increment("collaboration.disconnect", { documentName }); - metrics.gaugePerInstance("collaboration.connections_count", --count); + Metrics.increment("collaboration.disconnect", { documentName }); + Metrics.gaugePerInstance("collaboration.connections_count", --count); // TODO: Waiting for `instance` available in payload - // metrics.gaugePerInstance( + // Metrics.gaugePerInstance( // "collaboration.documents_count", // instance.documents.size() // ); } async onChange({ documentName }: { documentName: string }) { - metrics.increment("collaboration.change", { documentName }); + Metrics.increment("collaboration.change", { documentName }); } } diff --git a/server/commands/collectionImporter.js b/server/commands/collectionImporter.js index 40722aa3..9625fee5 100644 --- a/server/commands/collectionImporter.js +++ b/server/commands/collectionImporter.js @@ -2,20 +2,18 @@ import fs from "fs"; import os from "os"; import path from "path"; -import debug from "debug"; import File from "formidable/lib/file"; import invariant from "invariant"; import { values, keys } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { parseOutlineExport } from "../../shared/utils/zip"; import { FileImportError } from "../errors"; +import Logger from "../logging/logger"; import { Attachment, Event, Document, Collection, User } from "../models"; import attachmentCreator from "./attachmentCreator"; import documentCreator from "./documentCreator"; import documentImporter from "./documentImporter"; -const log = debug("commands"); - export default async function collectionImporter({ file, type, @@ -155,7 +153,7 @@ export default async function collectionImporter({ continue; } - log(`Skipped importing ${item.path}`); + Logger.info("commands", `Skipped importing ${item.path}`); } // All collections, documents, and attachments have been created – time to diff --git a/server/commands/documentPermanentDeleter.js b/server/commands/documentPermanentDeleter.js index a87c2621..6408271a 100644 --- a/server/commands/documentPermanentDeleter.js +++ b/server/commands/documentPermanentDeleter.js @@ -1,11 +1,9 @@ // @flow -import debug from "debug"; +import Logger from "../logging/logger"; import { Document, Attachment } from "../models"; import { sequelize } from "../sequelize"; import parseAttachmentIds from "../utils/parseAttachmentIds"; -const log = debug("commands"); - export async function documentPermanentDeleter(documents: Document[]) { const activeDocument = documents.find((doc) => !doc.deletedAt); @@ -47,9 +45,9 @@ export async function documentPermanentDeleter(documents: Document[]) { if (attachment) { await attachment.destroy(); - log(`Attachment ${attachmentId} deleted`); + Logger.info("commands", `Attachment ${attachmentId} deleted`); } else { - log(`Unknown attachment ${attachmentId} ignored`); + Logger.info("commands", `Unknown attachment ${attachmentId} ignored`); } } } diff --git a/server/commands/teamCreator.js b/server/commands/teamCreator.js index 21b961a8..9a05cc61 100644 --- a/server/commands/teamCreator.js +++ b/server/commands/teamCreator.js @@ -1,13 +1,11 @@ // @flow -import debug from "debug"; import { MaximumTeamsError } from "../errors"; +import Logger from "../logging/logger"; import { Team, AuthenticationProvider } from "../models"; import { sequelize } from "../sequelize"; import { getAllowedDomains } from "../utils/authentication"; import { generateAvatarUrl } from "../utils/avatars"; -const log = debug("server"); - type TeamCreatorResult = {| team: Team, authenticationProvider: AuthenticationProvider, @@ -111,7 +109,10 @@ export default async function teamCreator({ try { await team.provisionSubdomain(subdomain); } catch (err) { - log(`Provisioning subdomain failed: ${err.message}`); + Logger.error("Provisioning subdomain failed", err, { + teamId: team.id, + subdomain, + }); } return { diff --git a/server/index.js b/server/index.js index b9144566..e12122f0 100644 --- a/server/index.js +++ b/server/index.js @@ -3,7 +3,6 @@ import env from "./env"; // eslint-disable-line import/order import "./tracing"; // must come before importing any instrumented module import http from "http"; -import debug from "debug"; import Koa from "koa"; import compress from "koa-compress"; import helmet from "koa-helmet"; @@ -13,9 +12,10 @@ import Router from "koa-router"; import { uniq } from "lodash"; import stoppable from "stoppable"; import throng from "throng"; +import Logger from "./logging/logger"; +import { requestErrorHandler } from "./logging/sentry"; import services from "./services"; import { getArg } from "./utils/args"; -import { requestErrorHandler } from "./utils/sentry"; import { checkEnv, checkMigrations } from "./utils/startup"; import { checkUpdates } from "./utils/updates"; @@ -55,12 +55,12 @@ async function start(id: string, disconnect: () => void) { const app = new Koa(); const server = stoppable(http.createServer(app.callback())); - const httpLogger = debug("http"); - const log = debug("server"); const router = new Router(); // install basic middleware shared by all services - app.use(logger((str, args) => httpLogger(str))); + if ((env.DEBUG || "").includes("http")) { + app.use(logger((str, args) => Logger.info("http", str))); + } app.use(compress()); app.use(helmet()); @@ -87,7 +87,7 @@ async function start(id: string, disconnect: () => void) { throw new Error(`Unknown service ${name}`); } - log(`Starting ${name} service…`); + Logger.info("lifecycle", `Starting ${name} service`); const init = services[name]; await init(app, server); } @@ -98,7 +98,7 @@ async function start(id: string, disconnect: () => void) { server.on("listening", () => { const address = server.address(); - console.log(`\n> Listening on http://localhost:${address.port}\n`); + Logger.info("lifecycle", `Listening on http://localhost:${address.port}`); }); server.listen(normalizedPortFlag || env.PORT || "3000"); @@ -107,7 +107,7 @@ async function start(id: string, disconnect: () => void) { process.once("SIGINT", shutdown); function shutdown() { - console.log("\n> Stopping server"); + Logger.info("lifecycle", "Stopping server"); server.stop(disconnect); } } diff --git a/server/logging/logger.js b/server/logging/logger.js new file mode 100644 index 00000000..6098e8a0 --- /dev/null +++ b/server/logging/logger.js @@ -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(); diff --git a/server/logging/metrics.js b/server/logging/metrics.js new file mode 100644 index 00000000..ea0af059 --- /dev/null +++ b/server/logging/metrics.js @@ -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(); diff --git a/server/utils/sentry.js b/server/logging/sentry.js similarity index 100% rename from server/utils/sentry.js rename to server/logging/sentry.js diff --git a/server/mailer.js b/server/mailer.js index 6b84ecc0..eeedb5ac 100644 --- a/server/mailer.js +++ b/server/mailer.js @@ -1,6 +1,4 @@ // @flow -import * as Sentry from "@sentry/node"; -import debug from "debug"; import nodemailer from "nodemailer"; import Oy from "oy-vey"; import * as React from "react"; @@ -31,9 +29,9 @@ import { import { SigninEmail, signinEmailText } from "./emails/SigninEmail"; import { WelcomeEmail, welcomeEmailText } from "./emails/WelcomeEmail"; import { baseStyles } from "./emails/components/EmailLayout"; +import Logger from "./logging/logger"; import { emailsQueue } from "./queues"; -const log = debug("emails"); const useTestEmailService = process.env.NODE_ENV === "development" && !process.env.SMTP_USERNAME; @@ -101,7 +99,10 @@ export class Mailer { } if (useTestEmailService) { - log("SMTP_USERNAME not provided, generating test account…"); + Logger.info( + "email", + "SMTP_USERNAME not provided, generating test account…" + ); try { let testAccount = await nodemailer.createTestAccount(); @@ -118,7 +119,10 @@ export class Mailer { this.transporter = nodemailer.createTransport(smtpConfig); } catch (err) { - log(`Could not generate test account: ${err.message}`); + Logger.error( + "Couldn't generate a test account with ethereal.email", + err + ); } } } @@ -134,7 +138,7 @@ export class Mailer { }); try { - log(`Sending email "${data.title}" to ${data.to}`); + Logger.info("email", `Sending email "${data.title}" to ${data.to}`); const info = await transporter.sendMail({ from: process.env.SMTP_FROM_EMAIL, replyTo: process.env.SMTP_REPLY_EMAIL || process.env.SMTP_FROM_EMAIL, @@ -145,12 +149,13 @@ export class Mailer { }); if (useTestEmailService) { - log("Email Preview URL: %s", nodemailer.getTestMessageUrl(info)); + Logger.info( + "email", + `Preview Url: ${nodemailer.getTestMessageUrl(info)}` + ); } } catch (err) { - if (process.env.SENTRY_DSN) { - Sentry.captureException(err); - } + Logger.error(`Error sending email to ${data.to}`, err); throw err; // Re-throw for queue to re-try } } diff --git a/server/middlewares/passport.js b/server/middlewares/passport.js index b2363e12..8f3d9711 100644 --- a/server/middlewares/passport.js +++ b/server/middlewares/passport.js @@ -2,6 +2,7 @@ import passport from "@outlinewiki/koa-passport"; import { type Context } from "koa"; import type { AccountProvisionerResult } from "../commands/accountProvisioner"; +import Logger from "../logging/logger"; import { signIn } from "../utils/authentication"; export default function createMiddleware(providerName: string) { @@ -11,7 +12,7 @@ export default function createMiddleware(providerName: string) { { session: false }, async (err, user, result: AccountProvisionerResult) => { if (err) { - console.error(err); + Logger.error("Error during authentication", err); if (err.id) { const notice = err.id.replace(/_/g, "-"); @@ -36,7 +37,10 @@ export default function createMiddleware(providerName: string) { // Correlation ID, Timestamp in these two query string parameters. const { error, error_description } = ctx.request.query; if (error && error_description) { - console.error(error_description); + Logger.error( + "Error from Azure during authentication", + new Error(error_description) + ); // Display only the descriptive message to the user, log the rest const description = error_description.split("Trace ID")[0]; diff --git a/server/models/Team.js b/server/models/Team.js index 20f229e3..c796e3db 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -8,6 +8,7 @@ import { stripSubdomain, RESERVED_SUBDOMAINS, } from "../../shared/utils/domains"; +import Logger from "../logging/logger"; import { DataTypes, sequelize, Op } from "../sequelize"; import { generateAvatarUrl } from "../utils/avatars"; import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3"; @@ -134,8 +135,7 @@ const uploadAvatar = async (model) => { ); if (newUrl) model.avatarUrl = newUrl; } catch (err) { - // we can try again next time - console.error(err); + Logger.error("Error uploading avatar to S3", err, { url: avatarUrl }); } } }; diff --git a/server/models/User.js b/server/models/User.js index 9d592072..f9df293a 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -5,6 +5,7 @@ import JWT from "jsonwebtoken"; import { v4 as uuidv4 } from "uuid"; import { languages } from "../../shared/i18n"; import { ValidationError } from "../errors"; +import Logger from "../logging/logger"; import { DataTypes, sequelize, encryptedFields, Op } from "../sequelize"; import { DEFAULT_AVATAR_HOST } from "../utils/avatars"; import { palette } from "../utils/color"; @@ -195,8 +196,9 @@ const uploadAvatar = async (model) => { ); if (newUrl) model.avatarUrl = newUrl; } catch (err) { - // we can try again next time - console.error(err); + Logger.error("Couldn't upload user avatar image to S3", err, { + url: avatarUrl, + }); } } }; diff --git a/server/queues/processors/exports.js b/server/queues/processors/exports.js index 083b757c..9a61a370 100644 --- a/server/queues/processors/exports.js +++ b/server/queues/processors/exports.js @@ -1,14 +1,12 @@ // @flow import fs from "fs"; -import debug from "debug"; +import Logger from "../../logging/logger"; import mailer from "../../mailer"; import { FileOperation, Collection, Event, Team, User } from "../../models"; import type { Event as TEvent } from "../../types"; import { uploadToS3FromBuffer } from "../../utils/s3"; import { archiveCollections } from "../../utils/zip"; -const log = debug("commands"); - export default class ExportsProcessor { async on(event: TEvent) { switch (event.name) { @@ -30,7 +28,10 @@ export default class ExportsProcessor { }); // heavy lifting of creating the zip file - log(`Archiving collections for file operation ${exportData.id}`); + Logger.info( + "processor", + `Archiving collections for file operation ${exportData.id}` + ); const filePath = await archiveCollections(collections); let url, state; @@ -43,7 +44,10 @@ export default class ExportsProcessor { size: stat.size, }); - log(`Uploading archive for file operation ${exportData.id}`); + Logger.info( + "processor", + `Uploading archive for file operation ${exportData.id}` + ); url = await uploadToS3FromBuffer( readBuffer, "application/zip", @@ -51,10 +55,15 @@ export default class ExportsProcessor { "private" ); - log(`Upload complete for file operation ${exportData.id}`); + Logger.info( + "processor", + `Upload complete for file operation ${exportData.id}` + ); state = "complete"; - } catch (e) { - log("Failed to export data", e); + } catch (error) { + Logger.error("Error exporting collection data", error, { + fileOperationId: exportData.id, + }); state = "error"; url = null; } finally { diff --git a/server/queues/processors/notifications.js b/server/queues/processors/notifications.js index 2617578b..1a52ba4a 100644 --- a/server/queues/processors/notifications.js +++ b/server/queues/processors/notifications.js @@ -1,5 +1,5 @@ // @flow -import debug from "debug"; +import Logger from "../../logging/logger"; import mailer from "../../mailer"; import { View, @@ -12,8 +12,6 @@ import { import { Op } from "../../sequelize"; import type { DocumentEvent, CollectionEvent, Event } from "../../types"; -const log = debug("services"); - export default class NotificationsProcessor { async on(event: Event) { switch (event.name) { @@ -98,7 +96,8 @@ export default class NotificationsProcessor { }); if (view) { - log( + Logger.info( + "processor", `suppressing notification to ${setting.userId} because update viewed` ); continue; diff --git a/server/redis.js b/server/redis.js index 42b10a9f..fd8c921b 100644 --- a/server/redis.js +++ b/server/redis.js @@ -1,10 +1,11 @@ // @flow import Redis from "ioredis"; +import Logger from "./logging/logger"; const options = { maxRetriesPerRequest: 20, retryStrategy(times) { - console.warn(`Retrying redis connection: attempt ${times}`); + Logger.warn(`Retrying redis connection: attempt ${times}`); return Math.min(times * 100, 3000); }, // support Heroku Redis, see: diff --git a/server/routes/api/utils.js b/server/routes/api/utils.js index d5615147..96b9e532 100644 --- a/server/routes/api/utils.js +++ b/server/routes/api/utils.js @@ -1,14 +1,13 @@ // @flow import { subDays } from "date-fns"; -import debug from "debug"; import Router from "koa-router"; import { documentPermanentDeleter } from "../../commands/documentPermanentDeleter"; import { AuthenticationError } from "../../errors"; +import Logger from "../../logging/logger"; import { Document, FileOperation } from "../../models"; import { Op } from "../../sequelize"; const router = new Router(); -const log = debug("utils"); router.post("utils.gc", async (ctx) => { const { token, limit = 500 } = ctx.body; @@ -17,7 +16,10 @@ router.post("utils.gc", async (ctx) => { throw new AuthenticationError("Invalid secret token"); } - log(`Permanently destroying upto ${limit} documents older than 30 days…`); + Logger.info( + "utils", + `Permanently destroying upto ${limit} documents older than 30 days…` + ); const documents = await Document.scope("withUnpublished").findAll({ attributes: ["id", "teamId", "text", "deletedAt"], @@ -32,9 +34,12 @@ router.post("utils.gc", async (ctx) => { const countDeletedDocument = await documentPermanentDeleter(documents); - log(`Destroyed ${countDeletedDocument} documents`); + Logger.info("utils", `Destroyed ${countDeletedDocument} documents`); - log(`Expiring all the collection export older than 30 days…`); + Logger.info( + "utils", + `Expiring all the collection export older than 30 days…` + ); const exports = await FileOperation.unscoped().findAll({ where: { diff --git a/server/routes/auth/index.js b/server/routes/auth/index.js index 3fcfb3d5..c6886bc7 100644 --- a/server/routes/auth/index.js +++ b/server/routes/auth/index.js @@ -1,7 +1,6 @@ // @flow import passport from "@outlinewiki/koa-passport"; import { addMonths } from "date-fns"; -import debug from "debug"; import Koa from "koa"; import bodyParser from "koa-body"; import Router from "koa-router"; @@ -11,7 +10,6 @@ import validation from "../../middlewares/validation"; import { Collection, Team, View } from "../../models"; import providers from "./providers"; -const log = debug("server"); const app = new Koa(); const router = new Router(); @@ -21,7 +19,6 @@ router.use(passport.initialize()); providers.forEach((provider) => { if (provider.enabled) { router.use("/", provider.router.routes()); - log(`loaded ${provider.name} auth provider`); } }); diff --git a/server/scripts/20210226232041-migrate-authentication.js b/server/scripts/20210226232041-migrate-authentication.js index 5d092871..e0ffc5a6 100644 --- a/server/scripts/20210226232041-migrate-authentication.js +++ b/server/scripts/20210226232041-migrate-authentication.js @@ -1,6 +1,6 @@ // @flow import "./bootstrap"; -import debug from "debug"; +import Logger from "../logging/logger"; import { Team, User, @@ -9,14 +9,13 @@ import { } from "../models"; import { Op } from "../sequelize"; -const log = debug("server"); const cache = {}; let page = 0; let limit = 100; export default async function main(exit = false) { const work = async (page: number) => { - log(`Migrating authentication data… page ${page}`); + Logger.info("database", "Starting authentication migration"); const users = await User.findAll({ limit, @@ -42,13 +41,15 @@ export default async function main(exit = false) { const provider = user.service; const providerId = user.team[`${provider}Id`]; if (!providerId) { - console.error( - `user ${user.id} has serviceId ${user.serviceId}, but team ${provider}Id missing` + Logger.info( + "database", + `User ${user.id} has serviceId ${user.serviceId}, but team ${provider}Id missing` ); continue; } if (providerId.startsWith("transferred")) { - console.log( + Logger.info( + "database", `skipping previously transferred ${user.team.name} (${user.team.id})` ); continue; @@ -78,7 +79,8 @@ export default async function main(exit = false) { userId: user.id, }); } catch (err) { - console.error( + Logger.info( + "database", `serviceId ${user.serviceId} exists, for user ${user.id}` ); continue; @@ -91,7 +93,7 @@ export default async function main(exit = false) { await work(page); if (exit) { - log("Migration complete"); + Logger.info("database", "Migration complete"); process.exit(0); } } diff --git a/server/sequelize.js b/server/sequelize.js index f0318984..4091ab00 100644 --- a/server/sequelize.js +++ b/server/sequelize.js @@ -1,7 +1,7 @@ // @flow -import debug from "debug"; import Sequelize from "sequelize"; import EncryptedField from "sequelize-encrypted"; +import Logger from "./logging/logger"; const isProduction = process.env.NODE_ENV === "production"; const isSSLDisabled = process.env.PGSSLMODE === "disable"; @@ -15,7 +15,7 @@ export const Op = Sequelize.Op; export const sequelize = new Sequelize( process.env.DATABASE_URL || process.env.DATABASE_CONNECTION_POOL_URL, { - logging: debug("sql"), + logging: (msg) => Logger.debug("database", msg), typeValidation: true, dialectOptions: { ssl: diff --git a/server/services/web.js b/server/services/web.js index ce6b7544..75b8c17c 100644 --- a/server/services/web.js +++ b/server/services/web.js @@ -10,6 +10,7 @@ import mount from "koa-mount"; import enforceHttps from "koa-sslify"; import emails from "../emails"; import env from "../env"; +import Logger from "../logging/logger"; import routes from "../routes"; import api from "../routes/api"; import auth from "../routes/auth"; @@ -44,7 +45,7 @@ export default function init(app: Koa = new Koa(), server?: http.Server): Koa { }) ); } else { - console.warn("Enforced https was disabled with FORCE_HTTPS env variable"); + Logger.warn("Enforced https was disabled with FORCE_HTTPS env variable"); } // trust header fields set by our proxy. eg X-Forwarded-For @@ -90,7 +91,7 @@ export default function init(app: Koa = new Koa(), server?: http.Server): Koa { app.use( convert( hotMiddleware(compile, { - log: console.log, // eslint-disable-line + log: (...args) => Logger.info("lifecycle", ...args), path: "/__webpack_hmr", heartbeat: 10 * 1000, }) diff --git a/server/services/websockets.js b/server/services/websockets.js index 0179f43a..45642385 100644 --- a/server/services/websockets.js +++ b/server/services/websockets.js @@ -4,15 +4,14 @@ import Koa from "koa"; import IO from "socket.io"; import socketRedisAdapter from "socket.io-redis"; import SocketAuth from "socketio-auth"; -import env from "../env"; +import Logger from "../logging/logger"; +import Metrics from "../logging/metrics"; import { Document, Collection, View } from "../models"; import policy from "../policies"; import { websocketsQueue } from "../queues"; import WebsocketsProcessor from "../queues/processors/websockets"; import { client, subscriber } from "../redis"; import { getUserForJWT } from "../utils/jwt"; -import * as metrics from "../utils/metrics"; -import Sentry from "../utils/sentry"; const { can } = policy; @@ -37,23 +36,23 @@ export default function init(app: Koa, server: http.Server) { io.of("/").adapter.on("error", (err) => { if (err.name === "MaxRetriesPerRequestError") { - console.error(`Redis error: ${err.message}. Shutting down now.`); + Logger.error("Redis maximum retries exceeded in socketio adapter", err); throw err; } else { - console.error(`Redis error: ${err.message}`); + Logger.error("Redis error in socketio adapter", err); } }); io.on("connection", (socket) => { - metrics.increment("websockets.connected"); - metrics.gaugePerInstance( + Metrics.increment("websockets.connected"); + Metrics.gaugePerInstance( "websockets.count", socket.client.conn.server.clientsCount ); socket.on("disconnect", () => { - metrics.increment("websockets.disconnected"); - metrics.gaugePerInstance( + Metrics.increment("websockets.disconnected"); + Metrics.gaugePerInstance( "websockets.count", socket.client.conn.server.clientsCount ); @@ -106,7 +105,7 @@ export default function init(app: Koa, server: http.Server) { if (can(user, "read", collection)) { socket.join(`collection-${event.collectionId}`, () => { - metrics.increment("websockets.collections.join"); + Metrics.increment("websockets.collections.join"); }); } } @@ -127,7 +126,7 @@ export default function init(app: Koa, server: http.Server) { ); socket.join(room, () => { - metrics.increment("websockets.documents.join"); + Metrics.increment("websockets.documents.join"); // let everyone else in the room know that a new user joined io.to(room).emit("user.join", { @@ -139,14 +138,9 @@ export default function init(app: Koa, server: http.Server) { // let this user know who else is already present in the room io.in(room).clients(async (err, sockets) => { if (err) { - if (process.env.SENTRY_DSN) { - Sentry.withScope(function (scope) { - scope.setExtra("clients", sockets); - Sentry.captureException(err); - }); - } else { - console.error(err); - } + Logger.error("Error getting clients for room", err, { + sockets, + }); return; } @@ -173,13 +167,13 @@ export default function init(app: Koa, server: http.Server) { socket.on("leave", (event) => { if (event.collectionId) { socket.leave(`collection-${event.collectionId}`, () => { - metrics.increment("websockets.collections.leave"); + Metrics.increment("websockets.collections.leave"); }); } if (event.documentId) { const room = `document-${event.documentId}`; socket.leave(room, () => { - metrics.increment("websockets.documents.leave"); + Metrics.increment("websockets.documents.leave"); io.to(room).emit("user.leave", { userId: user.id, @@ -204,7 +198,7 @@ export default function init(app: Koa, server: http.Server) { }); socket.on("presence", async (event) => { - metrics.increment("websockets.presence"); + Metrics.increment("websockets.presence"); const room = `document-${event.documentId}`; @@ -232,14 +226,7 @@ export default function init(app: Koa, server: http.Server) { websocketsQueue.process(async function websocketEventsProcessor(job) { const event = job.data; websockets.on(event, io).catch((error) => { - if (env.SENTRY_DSN) { - Sentry.withScope(function (scope) { - scope.setExtra("event", event); - Sentry.captureException(error); - }); - } else { - throw error; - } + Logger.error("Error processing websocket event", error, { event }); }); }); } diff --git a/server/services/worker.js b/server/services/worker.js index 8b9150b6..0dbcf76e 100644 --- a/server/services/worker.js +++ b/server/services/worker.js @@ -1,7 +1,7 @@ // @flow import http from "http"; -import debug from "debug"; import Koa from "koa"; +import Logger from "../logging/logger"; import { globalEventQueue, processorEventQueue, @@ -16,9 +16,6 @@ import Imports from "../queues/processors/imports"; import Notifications from "../queues/processors/notifications"; import Revisions from "../queues/processors/revisions"; import Slack from "../queues/processors/slack"; -import Sentry from "../utils/sentry"; - -const log = debug("queue"); const EmailsProcessor = new Emails(); @@ -46,24 +43,22 @@ export default function init(app: Koa, server?: http.Server) { const event = job.data; const processor = eventProcessors[event.service]; if (!processor) { - console.warn( - `Received event for processor that isn't registered (${event.service})` - ); + Logger.warn(`Received event for processor that isn't registered`, event); return; } if (processor.on) { - log(`${event.service} processing ${event.name}`); + Logger.info("processor", `${event.service} processing ${event.name}`, { + name: event.name, + modelId: event.modelId, + }); processor.on(event).catch((error) => { - if (process.env.SENTRY_DSN) { - Sentry.withScope(function (scope) { - scope.setExtra("event", event); - Sentry.captureException(error); - }); - } else { - throw error; - } + Logger.error( + `Error processing ${event.name} in ${event.service}`, + error, + event + ); }); } }); @@ -72,14 +67,11 @@ export default function init(app: Koa, server?: http.Server) { const event = job.data; EmailsProcessor.on(event).catch((error) => { - if (process.env.SENTRY_DSN) { - Sentry.withScope(function (scope) { - scope.setExtra("event", event); - Sentry.captureException(error); - }); - } else { - throw error; - } + Logger.error( + `Error processing ${event.name} in emails processor`, + error, + event + ); }); }); } diff --git a/server/utils/authentication.js b/server/utils/authentication.js index 230eb689..a4336eab 100644 --- a/server/utils/authentication.js +++ b/server/utils/authentication.js @@ -1,9 +1,9 @@ // @flow import querystring from "querystring"; -import * as Sentry from "@sentry/node"; import { addMonths } from "date-fns"; import { type Context } from "koa"; import { pick } from "lodash"; +import Logger from "../logging/logger"; import { User, Event, Team, Collection, View } from "../models"; import { getCookieDomain } from "../utils/domains"; @@ -37,8 +37,8 @@ export async function signIn( ["ref", "utm_content", "utm_medium", "utm_source", "utm_campaign"] ); await team.update({ signupQueryParams }); - } catch (err) { - Sentry.captureException(err); + } catch (error) { + Logger.error(`Error persisting signup query params`, error); } } } diff --git a/server/utils/metrics.js b/server/utils/metrics.js deleted file mode 100644 index 9db6e129..00000000 --- a/server/utils/metrics.js +++ /dev/null @@ -1,45 +0,0 @@ -// @flow -import metrics from "datadog-metrics"; - -if (process.env.DD_API_KEY) { - metrics.init({ - apiKey: process.env.DD_API_KEY, - prefix: "outline.", - defaultTags: [`env:${process.env.DD_ENV || process.env.NODE_ENV}`], - }); -} - -export function gauge(key: string, value: number, tags?: string[]): void { - if (!process.env.DD_API_KEY) { - return; - } - - return metrics.gauge(key, value, tags); -} - -export function gaugePerInstance( - key: string, - value: number, - tags?: string[] = [] -): void { - if (!process.env.DD_API_KEY) { - 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 metrics.gauge(key, value, [...tags, `instance:${instanceId}`]); -} - -export function increment(key: string, tags?: { [string]: string }): void { - if (!process.env.DD_API_KEY) { - return; - } - - return metrics.increment(key, tags); -} diff --git a/server/utils/queue.js b/server/utils/queue.js index 8cfb826b..06484234 100644 --- a/server/utils/queue.js +++ b/server/utils/queue.js @@ -2,9 +2,8 @@ import Queue from "bull"; import Redis from "ioredis"; import { snakeCase } from "lodash"; +import Metrics from "../logging/metrics"; import { client, subscriber } from "../redis"; -import * as metrics from "../utils/metrics"; -import Sentry from "./sentry"; export function createQueue(name: string) { const prefix = `queue.${snakeCase(name)}`; @@ -26,29 +25,24 @@ export function createQueue(name: string) { }); queue.on("stalled", () => { - metrics.increment(`${prefix}.jobs.stalled`); + Metrics.increment(`${prefix}.jobs.stalled`); }); queue.on("completed", () => { - metrics.increment(`${prefix}.jobs.completed`); + Metrics.increment(`${prefix}.jobs.completed`); }); queue.on("error", (err) => { - if (process.env.SENTRY_DSN) { - Sentry.captureException(err); - } else { - console.error(err); - } - metrics.increment(`${prefix}.jobs.errored`); + Metrics.increment(`${prefix}.jobs.errored`); }); queue.on("failed", () => { - metrics.increment(`${prefix}.jobs.failed`); + Metrics.increment(`${prefix}.jobs.failed`); }); setInterval(async () => { - metrics.gauge(`${prefix}.count`, await queue.count()); - metrics.gauge(`${prefix}.delayed_count`, await queue.getDelayedCount()); + Metrics.gauge(`${prefix}.count`, await queue.count()); + Metrics.gauge(`${prefix}.delayed_count`, await queue.getDelayedCount()); }, 5 * 1000); return queue; diff --git a/server/utils/s3.js b/server/utils/s3.js index 57a03909..c806639b 100644 --- a/server/utils/s3.js +++ b/server/utils/s3.js @@ -1,10 +1,10 @@ // @flow import crypto from "crypto"; -import * as Sentry from "@sentry/node"; import AWS from "aws-sdk"; import { addHours, format } from "date-fns"; import fetch from "fetch-with-proxy"; import { v4 as uuidv4 } from "uuid"; +import Logger from "../logging/logger"; const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; @@ -147,15 +147,11 @@ export const uploadToS3FromUrl = async ( const endpoint = publicS3Endpoint(true); return `${endpoint}/${key}`; } catch (err) { - if (process.env.SENTRY_DSN) { - Sentry.captureException(err, { - extra: { - url, - }, - }); - } else { - throw err; - } + Logger.error("Error uploading to S3 from URL", err, { + url, + key, + acl, + }); } }; @@ -198,10 +194,8 @@ export const getFileByKey = async (key: string) => { const data = await s3.getObject(params).promise(); return data.Body; } catch (err) { - if (process.env.SENTRY_DSN) { - Sentry.captureException(err); - } else { - throw err; - } + Logger.error("Error getting file from S3 by key", err, { + key, + }); } }; diff --git a/server/utils/startup.js b/server/utils/startup.js index fb33069b..927cbe3e 100644 --- a/server/utils/startup.js +++ b/server/utils/startup.js @@ -1,5 +1,6 @@ // @flow import chalk from "chalk"; +import Logger from "../logging/logger"; import { Team, AuthenticationProvider } from "../models"; export async function checkMigrations() { @@ -11,12 +12,14 @@ export async function checkMigrations() { const providers = await AuthenticationProvider.count(); if (teams && !providers) { - console.error(` + Logger.warn( + ` This version of Outline cannot start until a data migration is complete. Backup your database, run the database migrations and the following script: $ node ./build/server/scripts/20210226232041-migrate-authentication.js -`); +` + ); process.exit(1); } } @@ -92,18 +95,16 @@ export function checkEnv() { } if (errors.length) { - console.log( - chalk.bold.red( - "\n\nThe server could not start, please fix the following configuration errors and try again:\n" - ) + Logger.warn( + "\n\nThe server could not start, please fix the following configuration errors and try again:\n" + + errors.map((e) => `- ${e}`).join("\n") ); - errors.map((text) => console.log(` - ${text}`)); - console.log("\n"); process.exit(1); } if (process.env.NODE_ENV === "production") { - console.log( + Logger.info( + "lifecycle", chalk.green( ` Is your team enjoying Outline? Consider supporting future development by sponsoring the project:\n\nhttps://github.com/sponsors/outline @@ -111,12 +112,12 @@ Is your team enjoying Outline? Consider supporting future development by sponsor ) ); } else if (process.env.NODE_ENV === "development") { - console.log( - chalk.yellow( - `\nRunning Outline in development mode. To run Outline in production mode set the ${chalk.bold( - "NODE_ENV" - )} env variable to "production"\n` - ) + Logger.warn( + `Running Outline in ${chalk.bold( + "development mode" + )}. To run Outline in production mode set the ${chalk.bold( + "NODE_ENV" + )} env variable to "production"` ); } } diff --git a/server/utils/zip.js b/server/utils/zip.js index b567d7e6..b9e1bf51 100644 --- a/server/utils/zip.js +++ b/server/utils/zip.js @@ -1,8 +1,8 @@ // @flow import fs from "fs"; -import * as Sentry from "@sentry/node"; import JSZip from "jszip"; import tmp from "tmp"; +import Logger from "../logging/logger"; import { Attachment, Collection, Document } from "../models"; import { serializeFilename } from "./fs"; import { getFileByKey } from "./s3"; @@ -47,11 +47,9 @@ async function addImageToArchive(zip, key) { const img = await getFileByKey(key); zip.file(key, img, { createFolders: true }); } catch (err) { - if (process.env.SENTRY_DSN) { - Sentry.captureException(err); - } - // error during file retrieval - console.error(err); + Logger.error("Error loading image attachment from S3", err, { + key, + }); } } diff --git a/yarn.lock b/yarn.lock index fb6e1b1d..7202b88e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1070,6 +1070,15 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + "@emotion/is-prop-valid@^0.8.2", "@emotion/is-prop-valid@^0.8.8": version "0.8.8" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" @@ -3248,6 +3257,11 @@ async@0.9.x: resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= +async@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" + integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -4468,7 +4482,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -4487,17 +4501,33 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.5.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312" + integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" + integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + colorette@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== -colors@^1.4.0: +colors@^1.2.1, colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -4507,6 +4537,14 @@ colors@~1.2.0-rc0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc" integrity sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg== +colorspace@1.1.x: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5" + integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ== + dependencies: + color "3.0.x" + text-hex "1.0.x" + combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -5597,6 +5635,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + encodeurl@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -6307,6 +6350,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -6319,6 +6367,11 @@ feature-policy@0.3.0: resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069" integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ== +fecha@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" + integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== + fetch-retry@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-4.1.1.tgz#fafe0bb22b54f4d0a9c788dff6dd7f8673ca63f3" @@ -6521,6 +6574,11 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + focus-visible@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3" @@ -7693,6 +7751,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-bigint@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" @@ -9089,6 +9152,11 @@ koalas@^1.0.2: resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" integrity sha1-MYQz8HQjXbePrlZhoCqMpT7ilc0= +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -9515,6 +9583,17 @@ lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.1 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +logform@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" + integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== + dependencies: + colors "^1.2.1" + fast-safe-stringify "^2.0.4" + fecha "^4.2.0" + ms "^2.1.1" + triple-beam "^1.3.0" + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -10034,9 +10113,9 @@ mz@2, mz@^2.4.0, mz@^2.6.0: thenify-all "^1.0.0" nan@^2.12.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== nanomatch@^1.2.9: version "1.2.13" @@ -10423,6 +10502,13 @@ once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: dependencies: wrappy "1" +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + onetime@^5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -11866,7 +11952,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -11889,7 +11975,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -12796,6 +12882,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -13143,6 +13236,11 @@ ssri@^8.0.0: dependencies: minipass "^3.1.1" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stack-utils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593" @@ -13623,6 +13721,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -13894,6 +13997,11 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +triple-beam@^1.2.0, triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -14788,6 +14896,29 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" +winston-transport@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" + integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== + dependencies: + readable-stream "^2.3.7" + triple-beam "^1.2.0" + +winston@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + wkx@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c"