diff --git a/experimental/traffic-portal/.eslintrc.json b/experimental/traffic-portal/.eslintrc.json index b3b69ce77c..620d628881 100644 --- a/experimental/traffic-portal/.eslintrc.json +++ b/experimental/traffic-portal/.eslintrc.json @@ -96,8 +96,7 @@ "error", { "allow": [ - "log", - "warn", + "trace", "dir", "timeLog", "assert", @@ -107,8 +106,6 @@ "group", "groupEnd", "table", - "dirxml", - "error", "groupCollapsed", "Console", "profile", diff --git a/experimental/traffic-portal/angular.json b/experimental/traffic-portal/angular.json index 5b2c52a685..a2fa69abff 100644 --- a/experimental/traffic-portal/angular.json +++ b/experimental/traffic-portal/angular.json @@ -133,7 +133,10 @@ "options": { "outputPath": "dist/traffic-portal/server", "main": "server.ts", - "tsConfig": "tsconfig.server.json" + "tsConfig": "tsconfig.server.json", + "sourceMap": true, + "buildOptimizer": false, + "optimization": false }, "configurations": { "production": { @@ -145,8 +148,8 @@ } ], "sourceMap": false, - "optimization": true, - "buildOptimizer": true + "optimization": true, + "buildOptimizer": true } } }, diff --git a/experimental/traffic-portal/middleware.ts b/experimental/traffic-portal/middleware.ts new file mode 100644 index 0000000000..28e0645e6b --- /dev/null +++ b/experimental/traffic-portal/middleware.ts @@ -0,0 +1,181 @@ +/** + * @license Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { opendir } from "fs/promises"; +import { join } from "path"; + +import type { NextFunction, Request, Response } from "express"; + +import { LogLevel, Logger } from "src/app/utils"; +import { environment } from "src/environments/environment"; + +import type { ServerConfig } from "./server.config"; + +/** + * StaticFile defines what compression files are available. + */ +interface StaticFile { + compressions: Array; +} + +/** + * CompressionType defines the different compression algorithms. + */ +interface CompressionType { + fileExt: string; + headerEncoding: string; + name: string; +} + +/** + * TPResponseLocals are the express.Response.locals properties specific to a + * response writer for the TP server. + */ +interface TPResponseLocals { + config: ServerConfig; + foundFiles: Map; + logger: Logger; + /** The time at which the request was received. */ + startTime: Date; + /** + * The time at which the response was finished being written (or + * `undefined` if not done yet). + */ + endTime?: Date | undefined; +} + +/** + * AuthenticatedResponse is a response writer for endpoints that require + * authentication. + */ +export type TPResponseWriter = Response; + +/** + * An HTTP request handler for the TP server. + */ +export type TPHandler = (req: Request, resp: TPResponseWriter, next: NextFunction) => void | PromiseLike; + +const gzip = { + fileExt: "gz", + headerEncoding: "gzip", + name: "gzip" +}; +const br = { + fileExt: "br", + headerEncoding: "br", + name: "brotli" +}; + +/** + * getFiles recursively gets all the files in a directory. + * + * @param path The path to get files from. + * @returns Files found in the directory. + */ +async function getFiles(path: string): Promise { + const dir = await opendir(path); + let dirEnt = await dir.read(); + let files = new Array(); + + while (dirEnt !== null) { + const name = join(path, dirEnt.name); + + if (dirEnt.isDirectory()) { + files = files.concat(await getFiles(name)); + } else { + files.push(name); + } + + dirEnt = await dir.read(); + } + await dir.close(); + + return files; +} + +/** + * loggingMiddleWare is a middleware factory for express.js that provides a + * logger. + * It does also provide a link to server configuration that can be used in + * handlers, and a couple other niceties. + * + * @param config The server configuration. + * @returns A middleware that adds a property `logger` to `resp.locals` for + * logging purposes. + */ +export async function loggingMiddleWare(config: ServerConfig): Promise { + const allFiles = await getFiles(config.browserFolder); + const compressedFiles = new Map( + allFiles.filter( + file => file.match(/\.(br|gz)$/) + ).map( + file => [file, undefined] + ) + ); + const foundFiles = new Map( + allFiles.filter( + file => file.match(/\.(js|css|tff|svg)$/) + ).map( + file => { + const staticFile: StaticFile = { + compressions: [] + }; + if (compressedFiles.has(`${file}.${br.fileExt}`)) { + staticFile.compressions.push(br); + } + if (compressedFiles.has(`${file}.${gzip.fileExt}`)) { + staticFile.compressions.push(gzip); + } + return [file, staticFile]; + } + ) + ); + + return async (req: Request, resp: TPResponseWriter, next: NextFunction): Promise => { + resp.locals.config = config; + const prefix = `${req.ip} HTTP/${req.httpVersion} ${req.method} ${req.url} ${req.hostname}`; + resp.locals.logger = new Logger(console, environment.production ? LogLevel.INFO : LogLevel.DEBUG, prefix); + resp.locals.logger.debug("handling"); + resp.locals.startTime = new Date(); + resp.locals.foundFiles = foundFiles; + + next(); + }; +} + +/** + * errorMiddleWare is a middleware for express.js that provides automatic + * handling of errors that aren't caught in the endpoint handlers. + * + * @param err Any error passed along by other handlers. + * @param _ The client request - unused. + * @param resp The server's response-writer + * @param next A function provided by Express which will call the next handler. + */ +export function errorMiddleWare(err: unknown, _: Request, resp: TPResponseWriter, next: NextFunction): void { + if (err !== null && err !== undefined) { + resp.locals.logger.error("unhandled error bubbled to routing:", String(err)); + if (!environment.production) { + console.trace(err); + } + if (!resp.locals.endTime) { + resp.status(502); // "Bad Gateway" + resp.write('{"alerts":[{"level":"error","text":"Unknown Traffic Portal server error occurred"}]}\n'); + resp.end("\n"); + resp.locals.endTime = new Date(); + next(err); + } + } +} diff --git a/experimental/traffic-portal/nightwatch/.eslintrc.json b/experimental/traffic-portal/nightwatch/.eslintrc.json index 700e866729..382602e237 100644 --- a/experimental/traffic-portal/nightwatch/.eslintrc.json +++ b/experimental/traffic-portal/nightwatch/.eslintrc.json @@ -43,7 +43,8 @@ "no-restricted-imports": [ "error", "../" - ] + ], + "no-console": "off" } } ] diff --git a/experimental/traffic-portal/server.config.ts b/experimental/traffic-portal/server.config.ts index ce552ca83b..83657b7f2a 100644 --- a/experimental/traffic-portal/server.config.ts +++ b/experimental/traffic-portal/server.config.ts @@ -12,11 +12,53 @@ * limitations under the License. */ +// Logging cannot be initialized until after the job of the routines in this +// file are complete. +/* eslint-disable no-console */ + import { execSync } from "child_process"; -import { existsSync, readFileSync } from "fs"; -import { join } from "path"; +import { access, constants, readFile, readdir, realpath } from "fs/promises"; +import { join, sep } from "path"; + import { hasProperty } from "src/app/utils"; +/** + * A Node system error. I don't know why but this isn't exposed by Node - it's a + * class but you won't be able to use `instanceof` - and isn't present in Node + * typings. I copied the properties and their descriptions from the NodeJS + * documentation. + */ +type SystemError = Error & { + /** If present, the address to which a network connection failed. */ + readonly address?: string; + /** The string error code. */ + readonly code: string; + /** If present, the file path destination when reporting a file system error. */ + readonly dest?: string; + /** The system-provided error number. */ + readonly errno: number; + /** If present, extra details about the error condition. */ + readonly info?: unknown; + /** A system-provided human-readable description of the error. */ + readonly message: string; + /** If present, the file path when reporting a file system error. */ + readonly path?: string; + /** If present, the network connection port that is not available. */ + readonly port?: number; + /** The name of the system call that triggered the error. */ + readonly syscall: string; +}; + +/** + * Checks if an {@link Error} is a {@link SystemError}. + * + * @param e The {@link Error} to check. + * @returns Whether `e` is a {@link SystemError}. + */ +function isSystemError(e: Error): e is SystemError { + return hasProperty(e, "code"); +} + /** * ServerVersion contains versioning information for the server, * consistent with what other components provide, even if some @@ -95,30 +137,25 @@ function isServerVersion(v: unknown): v is ServerVersion { return false; } - if (!Object.prototype.hasOwnProperty.call(v, "version")) { - console.error("version missing required field 'version'"); - return false; - } - if (typeof((v as {version: unknown}).version) !== "string") { + if (!hasProperty(v, "version", "string")) { + console.error("version required field 'version' missing or invalid"); return false; } - if (Object.prototype.hasOwnProperty.call(v, "commits") && (typeof((v as {commits: unknown}).commits)) !== "string") { - console.error(`version property 'commits' has incorrect type; want: string, got: ${typeof((v as {commits: unknown}).commits)}`); + if (hasProperty(v, "commits") && typeof(v.commits) !== "string") { + console.error(`version property 'commits' has incorrect type; want: string, got: ${typeof(v.commits)}`); return false; } - if (Object.prototype.hasOwnProperty.call(v, "hash") && (typeof((v as {hash: unknown}).hash)) !== "string") { - console.error(`version property 'hash' has incorrect type; want: string, got: ${typeof((v as {hash: unknown}).hash)}`); + if (hasProperty(v, "hash") && typeof(v.hash) !== "string") { + console.error(`version property 'hash' has incorrect type; want: string, got: ${typeof(v.hash)}`); return false; } - if (Object.prototype.hasOwnProperty.call(v, "elRelease") && (typeof((v as {elRelease: unknown}).elRelease)) !== "string") { - console.error( - `version property 'elRelease' has incorrect type; want: string, got: ${typeof (v as {elRelease: unknown}).elRelease}` - ); + if (hasProperty(v, "elRelease") && typeof(v.elRelease) !== "string") { + console.error(`version property 'elRelease' has incorrect type; want: string, got: ${typeof(v.elRelease)}`); return false; } - if (Object.prototype.hasOwnProperty.call(v, "arch") && (typeof((v as {arch: unknown}).arch)) !== "string") { - console.error(`version property 'arch' has incorrect type; want: string, got: ${typeof((v as {arch: unknown}).arch)}`); + if (hasProperty(v, "arch") && typeof(v.arch) !== "string") { + console.error(`version property 'arch' has incorrect type; want: string, got: ${typeof(v.arch)}`); return false; } return true; @@ -193,39 +230,27 @@ function isConfig(c: unknown): c is ServerConfig { } else { (c as {insecure: boolean}).insecure = false; } - if (!hasProperty(c, "port")) { - throw new Error("'port' is required"); + if (!hasProperty(c, "port", "number")) { + throw new Error("required configuration for 'port' is missing or not a valid number"); } - if (typeof(c.port) !== "number") { - throw new Error("'port' must be a number"); + if (!hasProperty(c, "trafficOps", "string")) { + throw new Error("required configuration for 'trafficOps' is missing or not a string"); } - if (!hasProperty(c, "trafficOps")){ - throw new Error("'trafficOps' is required"); + if (!hasProperty(c, "browserFolder", "string")) { + throw new Error("required configuration for 'browserFolder' is missing or not a string"); } - if (typeof(c.trafficOps) !== "string") { - throw new Error("'trafficOps' must be a string"); - } - if(!hasProperty(c, "tpv1Url")){ - throw new Error("'tpv1Url' is required"); - } - if (typeof(c.tpv1Url) !== "string") { - throw new Error("'tpv1Url' must be a string"); - } - if (!hasProperty(c, "browserFolder")) { - throw new Error("'browserFolder' is required"); - } - if (typeof(c.browserFolder) !== "string") { - throw new Error("'browserFolder' must be a string"); + if(!hasProperty(c, "tpv1Url", "string")){ + throw new Error("required configuration for 'tpv1Url' is missing or not a string"); } try { - c.trafficOps = new URL(c.trafficOps); + (c as {trafficOps: URL | string}).trafficOps = new URL(c.trafficOps); } catch (e) { throw new Error(`'trafficOps' is not a valid URL: ${e}`); } try { - c.tpv1Url = new URL(c.tpv1Url); + (c as {tpv1Url: URL | string}).tpv1Url = new URL(c.tpv1Url); } catch (e) { throw new Error(`'tpv1Url' is not a valid URL: ${e}`); } @@ -235,17 +260,11 @@ function isConfig(c: unknown): c is ServerConfig { throw new Error("'useSSL' must be a boolean"); } if (c.useSSL) { - if (!hasProperty(c, "certPath")) { - throw new Error("'certPath' is required to use SSL"); + if (!hasProperty(c, "certPath", "string")) { + throw new Error("missing or invalid 'certPath' - required to use SSL"); } - if (typeof(c.certPath) !== "string") { - throw new Error("'certPath' must be a string"); - } - if (!hasProperty(c, "keyPath")) { - throw new Error("'keyPath' is required to use SSL"); - } - if (typeof(c.keyPath) !== "string") { - throw new Error("'keyPath' must be a string"); + if (!hasProperty(c, "keyPath", "string")) { + throw new Error("missing or invalid 'keyPath' - required to use SSL"); } } } @@ -255,6 +274,30 @@ function isConfig(c: unknown): c is ServerConfig { const defaultVersionFile = "/etc/traffic-portal/version.json"; +/** + * Searches recursively upward through the filesystem to find a file named + * "VERSION" and returns the real, absolute path to that file. + * + * @param path The path from which to begin the search. + * @returns The path to the VERSION file, assuming it was found. + * @throws {Error} If no VERSION file could be found in `path` or any of its + * ancestor directories. + * @throws {SystemError} If the given path isn't a directory, or directory + * traversal fails for some reason. + */ +async function findVersionFile(path: string = "."): Promise { + for (const ent of await readdir(path)) { + if (ent === "VERSION") { + return realpath(join(path, ent)); + } + } + path = await realpath(join(path, "..")); + if (path === sep) { + throw new Error("VERSION file not found"); + } + return findVersionFile(path); +} + /** * Retrieves the server's version from the file path provided. * @@ -264,24 +307,36 @@ const defaultVersionFile = "/etc/traffic-portal/version.json"; * looking for the ATC VERSION file. * @returns The parsed server version. */ -export function getVersion(path?: string): ServerVersion { +export async function getVersion(path?: string): Promise { if (!path) { path = defaultVersionFile; } - if (existsSync(path)) { - const v = JSON.parse(readFileSync(path, {encoding: "utf8"})); + try { + const v = JSON.parse(await readFile(path, {encoding: "utf8"})); if (isServerVersion(v)) { return v; } throw new Error(`contents of version file '${path}' does not represent an ATC version`); + } catch (e) { + if (e instanceof Error && isSystemError(e)) { + if (e.code !== "ENOENT") { + throw new Error(`file at "${path}" could not be read: ${e.message}`); + } + } else { + throw new Error(`file at "${path}" could not be read: ${e}`); + } } - if (!existsSync("../../../../VERSION")) { - throw new Error(`'${path}' doesn't exist and '../../../../VERSION' doesn't exist`); + let versionFilePath: string; + try { + versionFilePath = await findVersionFile(); + } catch (e) { + throw new Error(`'${path}' doesn't exist and couldn't find a VERSION file from which to read a server version: ${e}`); } + const ver: ServerVersion = { - version: readFileSync("../../../../VERSION", {encoding: "utf8"}).trimEnd() + version: (await readFile(versionFilePath, {encoding: "utf8"})).trimEnd() }; try { @@ -326,8 +381,8 @@ export const defaultConfig: ServerConfig = { browserFolder: "/opt/traffic-portal/browser", insecure: false, port: 4200, - trafficOps: new URL("https://example.com"), tpv1Url: new URL("https://example.com"), + trafficOps: new URL("https://example.com"), version: { version: "" } }; /** @@ -337,35 +392,41 @@ export const defaultConfig: ServerConfig = { * @param ver The version to use for the server. * @returns A full configuration for the server. */ -export function getConfig(args: Args, ver: ServerVersion): ServerConfig { +export async function getConfig(args: Args, ver: ServerVersion): Promise { let cfg = defaultConfig; cfg.version = ver; let readFromFile = false; - if (existsSync(args.configFile)) { - const cfgFromFile = JSON.parse(readFileSync(args.configFile, {encoding: "utf8"})); - try { - if (isConfig(cfgFromFile)) { - cfg = cfgFromFile; - cfg.version = ver; - } - } catch (err) { - throw new Error(`invalid configuration file at '${args.configFile}': ${err}`); + try { + const cfgFromFile = JSON.parse(await readFile(args.configFile, {encoding: "utf8"})); + if (isConfig(cfgFromFile)) { + cfg = cfgFromFile; + cfg.version = ver; + } else { + throw new Error("bad contents; doesn't represent a configuration file"); } readFromFile = true; - } else if (args.configFile !== defaultConfigFile) { - throw new Error(`no such configuration file: ${args.configFile}`); + } catch (err) { + const msg = `invalid configuration file at '${args.configFile}'`; + if (err instanceof Error) { + if (!isSystemError(err) || (err.code !== "ENOENT" || args.configFile !== defaultConfigFile)) { + throw new Error(`${msg}: ${err.message}`); + } + } else { + throw new Error(`${msg}: ${err}`); + } } - let folder = cfg.browserFolder; if(args.browserFolder !== defaultConfig.browserFolder) { - folder = args.browserFolder; + cfg.browserFolder = args.browserFolder; } - if(!existsSync(folder)) { - throw new Error(`no such folder: ${folder}`); - } - if(!existsSync(join(folder, "index.html"))) { - throw new Error(`no such browser file: ${join(folder, "index.html")}`); + + try { + if (!(await readdir(cfg.browserFolder)).includes("index.html")) { + throw new Error("directory doesn't include an 'index.html' file"); + } + } catch (e) { + throw new Error(`setting browser directory: ${e instanceof Error ? e.message : e}`); } if(args.port !== defaultConfig.port) { @@ -412,8 +473,8 @@ export function getConfig(args: Args, ver: ServerVersion): ServerConfig { insecure: cfg.insecure, keyPath: args.keyPath, port: cfg.port, - trafficOps: cfg.trafficOps, tpv1Url: cfg.tpv1Url, + trafficOps: cfg.trafficOps, useSSL: true, version: ver }; @@ -427,11 +488,15 @@ export function getConfig(args: Args, ver: ServerVersion): ServerConfig { } if (cfg.useSSL) { - if (!existsSync(cfg.certPath)) { - throw new Error(`no such certificate file: ${cfg.certPath}`); + try { + await access(cfg.certPath, constants.R_OK); + } catch (e) { + throw new Error(`checking certificate file "${cfg.certPath}": ${e instanceof Error ? e.message : e}`); } - if (!existsSync(cfg.keyPath)) { - throw new Error(`no such key file: ${cfg.keyPath}`); + try { + await access(cfg.keyPath, constants.R_OK); + } catch (e) { + throw new Error(`checking key file "${cfg.keyPath}": ${e instanceof Error ? e.message : e}`); } } diff --git a/experimental/traffic-portal/server.ts b/experimental/traffic-portal/server.ts index 78702d2ddc..853a1ae45d 100644 --- a/experimental/traffic-portal/server.ts +++ b/experimental/traffic-portal/server.ts @@ -13,7 +13,7 @@ */ import "zone.js/node"; -import { existsSync, readdirSync, readFileSync, statSync } from "fs"; +import { readFileSync } from "fs"; import { createServer as createRedirectServer } from "http"; import { createServer, request, RequestOptions } from "https"; import { join } from "path"; @@ -27,71 +27,103 @@ import { defaultConfigFile, getConfig, getVersion, - ServerConfig, + type ServerConfig, versionToString } from "server.config"; -import { AppServerModule } from "./src/main.server"; +import { hasProperty, Logger, LogLevel } from "src/app/utils"; +import { environment } from "src/environments/environment"; +import { AppServerModule } from "src/main.server"; -/** - * StaticFile defines what compression files are available. - */ -interface StaticFile { - compressions: Array; -} +import { errorMiddleWare, loggingMiddleWare, type TPResponseWriter } from "./middleware"; + +const typeMap = new Map([ + ["js", "application/javascript"], + ["css", "text/css"], + ["ttf", "font/ttf"], + ["svg", "image/svg+xml"] +]); /** - * CompressionType defines the different compression algorithms. + * A handler for serving files from compressed variants. + * + * @param req The client request. + * @param res Response writer. + * @param next A delegation to the next handler, to be called if this handler + * determines it can't write a response (which is always because this handler + * doesn't do that). + * @returns nothing. This is just required because we're returning void function + * calls. Not actually sure why, seems like a bug to me. */ -interface CompressionType { - fileExt: string; - headerEncoding: string; - name: string; -} +function compressedFileHandler(req: express.Request, res: TPResponseWriter, next: express.NextFunction): void { + const type = req.path.split(".").pop(); + if (type === undefined || !typeMap.has(type)) { + res.locals.logger.debug("unrecognized/non-compress-able file extension:", type); + return next(); + } + const path = join(res.locals.config.browserFolder, req.path.substring(1)); + const file = res.locals.foundFiles.get(path); + if(!file || file.compressions.length === 0) { + res.locals.logger.debug("file", path, "doesn't have any available compression"); + return next(); + } + const acceptedEncodings = req.acceptsEncodings(); + for(const compression of file.compressions) { + if (!acceptedEncodings.includes(compression.headerEncoding)) { + continue; + } + req.url = req.url.replace(`${req.path}`, `${req.path}.${compression.fileExt}`); + res.set("Content-Encoding", compression.headerEncoding); + res.set("Content-Type", typeMap.get(type)); + res.locals.logger.info("Serving", compression.name, "compressed file", req.path); + return next(); + } -const gzip = { - fileExt: "gz", - headerEncoding: "gzip", - name: "gzip" -}; -const br = { - fileExt: "br", - headerEncoding: "br", - name: "brotli" -}; + res.locals.logger.debug("no file found that matches an encoding the client accepts - serving uncompressed"); + next(); +} /** - * getFiles recursively gets all the files in a directory. + * A handler for proxy-ing the Traffic Ops API. * - * @param path The path to get files from. - * @returns Files found in the directory. + * @param req The client's request. + * @param res The server's response writer. */ -function getFiles(path: string): string[] { - const all = readdirSync(path) - .map(file => join(path, file)); - const dirs = all - .filter(file => statSync(file).isDirectory()); - let files = all - .filter(file => !statSync(file).isDirectory()); - for (const dir of dirs) { - files = files.concat(getFiles(dir)); +function toProxyHandler(req: express.Request, res: TPResponseWriter): void { + const {logger, config} = res.locals; + + logger.debug(`Making TO API request to \`${req.originalUrl}\``); + + const fwdRequest: RequestOptions = { + headers: req.headers, + host: config.trafficOps.hostname, + method: req.method, + path: req.originalUrl, + port: config.trafficOps.port, + rejectUnauthorized: !config.insecure, + }; + + try { + const proxyRequest = request(fwdRequest, r => { + res.writeHead(r.statusCode ?? 502, r.headers); + r.pipe(res); + }); + req.pipe(proxyRequest); + } catch (e) { + logger.error("proxy-ing request:", e); } - return files; + res.locals.endTime = new Date(); } -let config: ServerConfig; /** * The Express app is exported so that it can be used by serverless Functions. * - * @param serverConfig Server configuration + * @param serverConfig Server configuration. * @returns The Express.js application. */ -export function app(serverConfig: ServerConfig): express.Express { +export async function app(serverConfig: ServerConfig): Promise { const server = express(); const indexHtml = join(serverConfig.browserFolder, "index.html"); - if (!existsSync(indexHtml)) { - throw new Error(`Unable to start TP server, unable to find browser index.html at: ${indexHtml}`); - } // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) server.engine("html", ngExpressEngine({ @@ -101,101 +133,57 @@ export function app(serverConfig: ServerConfig): express.Express { server.set("view engine", "html"); server.set("views", "./"); - const allFiles = getFiles(serverConfig.browserFolder); - const compressedFiles = new Map(allFiles - .filter(file => file.match(/\.(br|gz)$/)) - .map(file => [file, undefined])); - const foundFiles = new Map(allFiles - .filter(file => file.match(/\.(js|css|tff|svg)$/)) - .map(file => { - const staticFile: StaticFile = { - compressions: [] - }; - if (compressedFiles.has(`${file}.${br.fileExt}`)) { - staticFile.compressions.push(br); - } - if (compressedFiles.has(`${file}.${gzip.fileExt}`)) { - staticFile.compressions.push(gzip); - } - return [file, staticFile]; - })); - - const typeMap = new Map([ - ["js", "application/javascript"], - ["css", "text/css"], - ["ttf", "font/ttf"], - ["svg", "image/svg+xml"] - ]); + // Express 4.x doesn't handle Promise rejections (need to be manually + // propagated with `next`), so it's not technically accurate to say that + // void Promises are the same as void. Using `async`, though, is so much + // easier than not doing that, so we're gonna go ahead and pretend that + // `Promise` is the same as `void`, in this one case. + // + // Note: Express 5.x fully supports async handlers - including seamless + // rejections - but it's still in beta at the time of this writing. + const loggingMW: express.RequestHandler = await loggingMiddleWare(serverConfig) as express.RequestHandler; + server.use(loggingMW); // Could just use express compression `server.use(compression())` but that is calculated for each request - server.get("*.(js|css|ttf|svg)", function(req, res, next) { - const type = req.path.split(".").pop(); - if (type === undefined || !typeMap.has(type)) { - return next(); - } - const path = join(serverConfig.browserFolder, req.path.substring(1, req.path.length)); - const file = foundFiles.get(path); - if(!file || file.compressions.length === 0) { - return next(); - } - const acceptedEncodings = req.acceptsEncodings(); - for(const compression of file.compressions) { - if (acceptedEncodings.indexOf(compression.headerEncoding) === -1) { - continue; - } - req.url = req.url.replace(`${req.path}`, `${req.path}.${compression.fileExt}`); - res.set("Content-Encoding", compression.headerEncoding); - res.set("Content-Type", typeMap.get(type)); - console.log(`Serving ${compression.name} compressed file ${req.path}`); - return next(); - } - next(); - }); - // Example Express Rest API endpoints - // server.get('/api/**', (req, res) => { }); - // Serve static files from /browser - server.get("*.*", express.static(serverConfig.browserFolder, { - maxAge: "1y" - })); - - /** - * A handler for proxying the Traffic Ops API. - * - * @param req The client's request. - * @param res The server's response writer. - */ - function toProxyHandler(req: express.Request, res: express.Response): void { - console.log(`Making TO API request to \`${req.originalUrl}\``); + server.get("*.(js|css|ttf|svg)", compressedFileHandler); - const fwdRequest: RequestOptions = { - headers: req.headers, - host: config.trafficOps.hostname, - method: req.method, - path: req.originalUrl, - port: config.trafficOps.port, - rejectUnauthorized: !config.insecure, - }; - - try { - const proxiedRequest = request(fwdRequest, (r) => { - res.writeHead(r.statusCode ?? 502, r.headers); - r.pipe(res); - }); - req.pipe(proxiedRequest); - } catch (e) { - console.error("proxying request:", e); + server.get( + "*.*", + (req, res: TPResponseWriter, next) => { + express.static(res.locals.config.browserFolder, {maxAge: "1y"})(req, res, next); + // Express's static handler doesn't call `next` and calling it + // yourself will break it for some reason, so we need to do this by + // hand here. + const elapsed = (new Date()).valueOf() - res.locals.startTime.valueOf(); + res.locals.logger.info("handled in", elapsed, "milliseconds with code", res.statusCode); } - } + ); server.use("api/**", toProxyHandler); server.use("/api/**", toProxyHandler); // All regular routes use the Universal engine - server.get("*", (req, res) => { - res.render(indexHtml, {providers: [ - {provide: APP_BASE_HREF, useValue: req.baseUrl}, - {provide: "TP_V1_URL", useValue: config.tpv1Url} - ], req}); + server.get("*", (req, res: TPResponseWriter) => { + res.render( + indexHtml, + { + providers: [ + {provide: APP_BASE_HREF, useValue: req.baseUrl}, + {provide: "TP_V1_URL", useValue: res.locals.config.tpv1Url}, + ], + req + }, + ); + res.locals.endTime = new Date(); + }); + + server.use(errorMiddleWare); + server.use((_, resp: TPResponseWriter) => { + if (!resp.locals.endTime) { + resp.locals.endTime = new Date(); + } + const elapsed = resp.locals.endTime.valueOf() - resp.locals.startTime.valueOf(); + resp.locals.logger.info("handled in", elapsed, "milliseconds with code", resp.statusCode); }); server.enable("trust proxy"); @@ -207,8 +195,8 @@ export function app(serverConfig: ServerConfig): express.Express { * * @returns An exit code for the process. */ -function run(): number { - const version = getVersion(); +async function run(): Promise { + const version = await getVersion(); const parser = new ArgumentParser({ // Nothing I can do about this, library specifies its interface. /* eslint-disable @typescript-eslint/naming-convention */ @@ -283,15 +271,20 @@ function run(): number { version: versionToString(version) }); + let config: ServerConfig; try { - config = getConfig(parser.parse_args(), version); + config = await getConfig(parser.parse_args(), version); } catch (e) { + // Logger cannot be initialized before reading server configuration + // eslint-disable-next-line no-console console.error(`Failed to initialize server configuration: ${e}`); return 1; } + const logger = new Logger(console, environment.production ? LogLevel.INFO : LogLevel.DEBUG); + // Start up the Node server - const server = app(config); + const server = await app(config); if (config.useSSL) { let cert: string; @@ -302,7 +295,10 @@ function run(): number { key = readFileSync(config.keyPath, {encoding: "utf8"}); ca = config.certificateAuthPaths.map(c => readFileSync(c, {encoding: "utf8"})); } catch (e) { - console.error("reading SSL key/cert:", e); + logger.error("reading SSL key/cert:", String(e)); + if (!environment.production) { + console.trace(e); + } return 1; } createServer( @@ -314,14 +310,14 @@ function run(): number { }, server ).listen(config.port, () => { - console.log(`Node Express server listening on port ${config.port}`); + logger.debug(`Node Express server listening on port ${config.port}`); }); try { const redirectServer = createRedirectServer( (req, res) => { if (!req.url) { res.statusCode = 500; - console.error("got HTTP request for redirect that had no URL"); + logger.error("got HTTP request for redirect that had no URL"); res.end(); return; } @@ -332,20 +328,18 @@ function run(): number { ); redirectServer.listen(80); redirectServer.on("error", e => { - console.error(`redirect server encountered error: ${e}`); - if (Object.prototype.hasOwnProperty.call(e, "code") && (e as typeof e & { - code: unknown; - }).code === "EACCES") { - console.warn("access to port 80 not allowed; closing redirect server"); + logger.error("redirect server encountered error:", String(e)); + if (hasProperty(e, "code", "string") && e.code === "EACCES") { + logger.warn("access to port 80 not allowed; closing redirect server"); redirectServer.close(); } }); } catch (e) { - console.warn("Failed to initialize HTTP-to-HTTPS redirect listener:", e); + logger.warn("Failed to initialize HTTP-to-HTTPS redirect listener:", e); } } else { server.listen(config.port, () => { - console.log(`Node Express server listening on port ${config.port}`); + logger.debug(`Node Express server listening on port ${config.port}`); }); } return 0; @@ -362,12 +356,17 @@ try { const mainModule = __non_webpack_require__.main; const moduleFilename = mainModule && mainModule.filename || ""; if (moduleFilename === __filename || moduleFilename.includes("iisnode")) { - const code = run(); - if (code) { - process.exit(code); - } + run().then( + code => { + if (code) { + process.exit(code); + } + } + ); } } catch (e) { + // Logger cannot be initialized before reading server configuration + // eslint-disable-next-line no-console console.error("Encountered error while running server:", e); process.exit(1); } diff --git a/experimental/traffic-portal/src/app/api/delivery-service.service.ts b/experimental/traffic-portal/src/app/api/delivery-service.service.ts index 714dfe9f03..186e045bfa 100644 --- a/experimental/traffic-portal/src/app/api/delivery-service.service.ts +++ b/experimental/traffic-portal/src/app/api/delivery-service.service.ts @@ -30,6 +30,8 @@ import type { TPSData, } from "src/app/models"; +import { LoggingService } from "../shared/logging.service"; + import { APIService } from "./base-api.service"; /** @@ -61,7 +63,9 @@ function defaultDataSet(label: string): DataSetWithSummary { */ export function constructDataSetFromResponse(r: DSStats): DataSetWithSummary { if (!r.series) { - console.error("raw DS stats response:", r); + // logging service not accessible in this scope + // eslint-disable-next-line no-console + console.debug("raw DS stats response:", r); throw new Error("invalid data set response"); } @@ -115,12 +119,7 @@ export class DeliveryServiceService extends APIService { /** This is where DS Types are cached, as they are presumed to not change (often). */ private deliveryServiceTypes: Array; - /** - * Injects the Angular HTTP client service into the parent constructor. - * - * @param http The Angular HTTP client service. - */ - constructor(http: HttpClient) { + constructor(http: HttpClient, private readonly log: LoggingService) { super(http); this.deliveryServiceTypes = new Array(); } @@ -308,7 +307,7 @@ export class DeliveryServiceService extends APIService { } return series; } - console.error("data response:", r); + this.log.debug("data response:", r); throw new Error("no data series found"); } return this.get(path, undefined, params).toPromise(); diff --git a/experimental/traffic-portal/src/app/api/misc-apis.service.ts b/experimental/traffic-portal/src/app/api/misc-apis.service.ts index 3ff154e2fc..03f334b19b 100644 --- a/experimental/traffic-portal/src/app/api/misc-apis.service.ts +++ b/experimental/traffic-portal/src/app/api/misc-apis.service.ts @@ -17,6 +17,7 @@ import { Injectable } from "@angular/core"; import type { ISORequest, OSVersions } from "trafficops-types"; import { AlertService } from "../shared/alert/alert.service"; +import { LoggingService } from "../shared/logging.service"; import { APIService, hasAlerts } from "./base-api.service"; @@ -29,7 +30,7 @@ import { APIService, hasAlerts } from "./base-api.service"; @Injectable() export class MiscAPIsService extends APIService{ - constructor(http: HttpClient, private readonly alertsService: AlertService) { + constructor(http: HttpClient, private readonly alertsService: AlertService, private readonly log: LoggingService) { super(http); } @@ -69,7 +70,7 @@ export class MiscAPIsService extends APIService{ body.alerts.forEach(a => this.alertsService.newAlert(a)); } } catch (innerError) { - console.error("during handling request failure, encountered an error trying to parse error-level alerts:", innerError); + this.log.error("during handling request failure, encountered an error trying to parse error-level alerts:", innerError); } throw new Error(`POST isos failed with status ${e.status} ${e.statusText}`); } @@ -79,7 +80,7 @@ export class MiscAPIsService extends APIService{ throw new Error(`POST isos returned no response body - ${response.status} ${response.statusText}`); } if (response.body.type !== "application/octet-stream") { - console.warn("data returned by TO for ISO generation is of unrecognized MIME type", response.body.type); + this.log.warn("data returned by TO for ISO generation is of unrecognized MIME type", response.body.type); } return response.body; } diff --git a/experimental/traffic-portal/src/app/api/server.service.ts b/experimental/traffic-portal/src/app/api/server.service.ts index 4b1afea5ba..be5c66a069 100644 --- a/experimental/traffic-portal/src/app/api/server.service.ts +++ b/experimental/traffic-portal/src/app/api/server.service.ts @@ -29,6 +29,8 @@ import type { ServerQueueResponse, } from "trafficops-types"; +import { LoggingService } from "../shared/logging.service"; + import { APIService } from "./base-api.service"; /** @@ -37,7 +39,7 @@ import { APIService } from "./base-api.service"; @Injectable() export class ServerService extends APIService { - constructor(http: HttpClient) { + constructor(http: HttpClient, private readonly log: LoggingService) { super(http); } @@ -83,7 +85,7 @@ export class ServerService extends APIService { // This is, unfortunately, possible, despite the many assumptions to // the contrary. if (servers.length > 1) { - console.warn( + this.log.warn( "Traffic Ops returned", servers.length, `servers with host name '${idOrName}' - selecting the first arbitrarily` diff --git a/experimental/traffic-portal/src/app/api/testing/user.service.ts b/experimental/traffic-portal/src/app/api/testing/user.service.ts index 66f35f9713..607e9f3ebe 100644 --- a/experimental/traffic-portal/src/app/api/testing/user.service.ts +++ b/experimental/traffic-portal/src/app/api/testing/user.service.ts @@ -27,6 +27,8 @@ import type { ResponseUser } from "trafficops-types"; +import { LoggingService } from "src/app/shared/logging.service"; + /** * Represents a request to register a user via email using the `/users/register` * API endpoint. @@ -103,6 +105,8 @@ export class UserService { private readonly tokens = new Map(); + constructor(private readonly log: LoggingService) {} + /** * Performs authentication with the Traffic Ops server. * @@ -120,19 +124,19 @@ export class UserService { public async login(uOrT: string, p?: string): Promise | null> { if (p !== undefined) { if (uOrT !== this.testAdminUsername || p !== this.testAdminPassword) { - console.error("Invalid username or password."); + this.log.error("Invalid username or password."); return null; } return new HttpResponse({body: {alerts: [{level: "success", text: "Successfully logged in."}]}, status: 200}); } const email = this.tokens.get(uOrT); if (email === undefined) { - console.error(`token '${uOrT}' did not match any set token for any user`); + this.log.error(`token '${uOrT}' did not match any set token for any user`); return null; } const user = this.users.find(u=>u.email === email); if (!user) { - console.error(`email '${email}' associated with token '${uOrT}' did not belong to any User`); + this.log.error(`email '${email}' associated with token '${uOrT}' did not belong to any User`); return null; } this.tokens.delete(uOrT); @@ -187,7 +191,7 @@ export class UserService { if (user) { return transformUser(user); } - console.warn("stored admin username not found in stored users: from now on the current user will be (more or less) random"); + this.log.warn("stored admin username not found in stored users: from now on the current user will be (more or less) random"); user = this.users[0]; if (!user) { throw new Error("no users exist"); @@ -204,7 +208,7 @@ export class UserService { public async updateCurrentUser(user: ResponseCurrentUser): Promise { const storedUser = this.users.findIndex(u=>u.id === user.id); if (storedUser < 0) { - console.error(`no such User: #${user.id}`); + this.log.error(`no such User: #${user.id}`); return false; } this.testAdminUsername = user.username; @@ -555,11 +559,11 @@ export class UserService { */ public async resetPassword(email: string): Promise { if (!this.users.some(u=>u.email === email)) { - console.error(`no User exists with email '${email}' - TO doesn't expose that information with an error, so neither will we`); + this.log.error(`no User exists with email '${email}' - TO doesn't expose that information with an error, so neither will we`); return; } const token = (Math.random() + 1).toString(36).substring(2); - console.log("setting token", token, "for email", email); + this.log.debug("setting token", token, "for email", email); this.tokens.set(token, email); } diff --git a/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.spec.ts b/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.spec.ts index 10f2a3492a..278140921a 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.spec.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.spec.ts @@ -18,19 +18,19 @@ import { RouterTestingModule } from "@angular/router/testing"; import { ReplaySubject } from "rxjs"; import { APITestingModule } from "src/app/api/testing"; -import { AsnDetailComponent } from "src/app/core/cache-groups/asns/detail/asn-detail.component"; +import { ASNDetailComponent } from "src/app/core/cache-groups/asns/detail/asn-detail.component"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; describe("AsnDetailComponent", () => { - let component: AsnDetailComponent; - let fixture: ComponentFixture; + let component: ASNDetailComponent; + let fixture: ComponentFixture; let route: ActivatedRoute; let paramMap: jasmine.Spy; const headerSvc = jasmine.createSpyObj([],{headerHidden: new ReplaySubject(), headerTitle: new ReplaySubject()}); beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ AsnDetailComponent ], + declarations: [ ASNDetailComponent ], imports: [ APITestingModule, RouterTestingModule, MatDialogModule ], providers: [ { provide: NavigationService, useValue: headerSvc } ] }) @@ -39,7 +39,7 @@ describe("AsnDetailComponent", () => { route = TestBed.inject(ActivatedRoute); paramMap = spyOn(route.snapshot.paramMap, "get"); paramMap.and.returnValue(null); - fixture = TestBed.createComponent(AsnDetailComponent); + fixture = TestBed.createComponent(ASNDetailComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -52,7 +52,7 @@ describe("AsnDetailComponent", () => { it("new asn", async () => { paramMap.and.returnValue("new"); - fixture = TestBed.createComponent(AsnDetailComponent); + fixture = TestBed.createComponent(ASNDetailComponent); component = fixture.componentInstance; fixture.detectChanges(); await fixture.whenStable(); @@ -65,7 +65,7 @@ describe("AsnDetailComponent", () => { it("existing asn", async () => { paramMap.and.returnValue("1"); - fixture = TestBed.createComponent(AsnDetailComponent); + fixture = TestBed.createComponent(ASNDetailComponent); component = fixture.componentInstance; fixture.detectChanges(); await fixture.whenStable(); diff --git a/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.ts index cb06437296..cc80a543f7 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/asns/detail/asn-detail.component.ts @@ -19,6 +19,7 @@ import { ResponseASN, ResponseCacheGroup } from "trafficops-types"; import { CacheGroupService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -29,13 +30,19 @@ import { NavigationService } from "src/app/shared/navigation/navigation.service" styleUrls: ["./asn-detail.component.scss"], templateUrl: "./asn-detail.component.html" }) -export class AsnDetailComponent implements OnInit { +export class ASNDetailComponent implements OnInit { public new = false; public asn!: ResponseASN; public cachegroups!: Array; - constructor(private readonly route: ActivatedRoute, private readonly cacheGroupService: CacheGroupService, - private readonly location: Location, private readonly dialog: MatDialog, - private readonly header: NavigationService) { + + constructor( + private readonly route: ActivatedRoute, + private readonly cacheGroupService: CacheGroupService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly header: NavigationService, + private readonly log: LoggingService, + ) { } /** @@ -45,7 +52,7 @@ export class AsnDetailComponent implements OnInit { this.cachegroups = await this.cacheGroupService.getCacheGroups(); const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -63,7 +70,7 @@ export class AsnDetailComponent implements OnInit { } const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number:", ID); + this.log.error("route parameter 'id' was non-number:", ID); return; } @@ -76,7 +83,7 @@ export class AsnDetailComponent implements OnInit { */ public async deleteAsn(): Promise { if (this.new) { - console.error("Unable to delete new ASN"); + this.log.error("Unable to delete new ASN"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.spec.ts b/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.spec.ts index 6524f51baa..12ce08366a 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.spec.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.spec.ts @@ -17,20 +17,20 @@ import { MatDialogModule } from "@angular/material/dialog"; import { RouterTestingModule } from "@angular/router/testing"; import { APITestingModule } from "src/app/api/testing"; -import { AsnsTableComponent } from "src/app/core/cache-groups/asns/table/asns-table.component"; +import { ASNsTableComponent } from "src/app/core/cache-groups/asns/table/asns-table.component"; describe("CacheGroupTableComponent", () => { - let component: AsnsTableComponent; - let fixture: ComponentFixture; + let component: ASNsTableComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ AsnsTableComponent ], + declarations: [ ASNsTableComponent ], imports: [ APITestingModule, RouterTestingModule, MatDialogModule ] }) .compileComponents(); - fixture = TestBed.createComponent(AsnsTableComponent); + fixture = TestBed.createComponent(ASNsTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.ts index 598abe1476..1ab0c37145 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/asns/table/asns-table.component.ts @@ -27,6 +27,7 @@ import type { ContextMenuItem, DoubleClickLink } from "src/app/shared/generic-table/generic-table.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -37,12 +38,18 @@ import { NavigationService } from "src/app/shared/navigation/navigation.service" styleUrls: ["./asns-table.component.scss"], templateUrl: "./asns-table.component.html" }) -export class AsnsTableComponent implements OnInit { +export class ASNsTableComponent implements OnInit { /** List of asns */ public asns: Promise>; - constructor(private readonly route: ActivatedRoute, private readonly headerSvc: NavigationService, - private readonly api: CacheGroupService, private readonly dialog: MatDialog, public readonly auth: CurrentUserService) { + constructor( + private readonly route: ActivatedRoute, + private readonly headerSvc: NavigationService, + private readonly api: CacheGroupService, + private readonly dialog: MatDialog, + public readonly auth: CurrentUserService, + private readonly log: LoggingService, + ) { this.fuzzySubject = new BehaviorSubject(""); this.asns = this.api.getASNs(); this.headerSvc.headerTitle.next("ASNs"); @@ -59,7 +66,7 @@ export class AsnsTableComponent implements OnInit { } }, e => { - console.error("Failed to get query parameters:", e); + this.log.error("Failed to get query parameters:", e); } ); } @@ -126,7 +133,7 @@ export class AsnsTableComponent implements OnInit { */ public async handleContextMenu(evt: ContextMenuActionEvent): Promise { if (Array.isArray(evt.data)) { - console.error("cannot delete multiple ASNs at once:", evt.data); + this.log.error("cannot delete multiple ASNs at once:", evt.data); return; } const data = evt.data; diff --git a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-details/cache-group-details.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-details/cache-group-details.component.ts index cc4b5280e0..7842b7b810 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-details/cache-group-details.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-details/cache-group-details.component.ts @@ -21,6 +21,7 @@ import { LocalizationMethod, localizationMethodToString, TypeFromResponse, type import { CacheGroupService, TypeService } from "src/app/api"; import { DecisionDialogComponent, type DecisionDialogData } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -72,7 +73,8 @@ export class CacheGroupDetailsComponent implements OnInit { private readonly typesAPI: TypeService, private readonly location: Location, private readonly dialog: MatDialog, - private readonly navSvc: NavigationService + private readonly navSvc: NavigationService, + private readonly log: LoggingService, ) { } @@ -84,7 +86,7 @@ export class CacheGroupDetailsComponent implements OnInit { public async ngOnInit(): Promise { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -104,7 +106,7 @@ export class CacheGroupDetailsComponent implements OnInit { await cgsPromise; const idx = this.cacheGroups.findIndex(c => c.id === numID); if (idx < 0) { - console.error(`no such Cache Group: #${ID}`); + this.log.error(`no such Cache Group: #${ID}`); return; } this.cacheGroup = this.cacheGroups.splice(idx, 1)[0]; @@ -165,7 +167,7 @@ export class CacheGroupDetailsComponent implements OnInit { */ public async delete(): Promise { if (this.new) { - console.error("Unable to delete new Cache Group"); + this.log.error("Unable to delete new Cache Group"); return; } const ref = this.dialog.open( @@ -199,7 +201,7 @@ export class CacheGroupDetailsComponent implements OnInit { } const {value} = this.typeCtrl; if (value === null) { - return console.error("cannot create Cache Group of null Type"); + return this.log.error("cannot create Cache Group of null Type"); } this.cacheGroup.typeId = value; this.cacheGroup.shortName = this.cacheGroup.name; diff --git a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.ts index 0ca1331cad..2bba71171a 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/cache-group-table/cache-group-table.component.ts @@ -39,6 +39,7 @@ import type { ContextMenuItem, DoubleClickLink } from "src/app/shared/generic-table/generic-table.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -200,7 +201,8 @@ export class CacheGroupTableComponent implements OnInit { private readonly dialog: MatDialog, private readonly alerts: AlertService, public readonly auth: CurrentUserService, - private readonly navSvc: NavigationService + private readonly navSvc: NavigationService, + private readonly log: LoggingService, ) { this.fuzzySubject = new BehaviorSubject(""); this.cacheGroups = this.api.getCacheGroups(); @@ -292,13 +294,13 @@ export class CacheGroupTableComponent implements OnInit { break; case "delete": if (Array.isArray(a.data)) { - console.error("cannot delete multiple cache groups at once:", a.data); + this.log.error("cannot delete multiple cache groups at once:", a.data); return; } this.delete(a.data); break; default: - console.error("unrecognized context menu action:", a.action); + this.log.error("unrecognized context menu action:", a.action); } } } diff --git a/experimental/traffic-portal/src/app/core/cache-groups/coordinates/detail/coordinate-detail.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/coordinates/detail/coordinate-detail.component.ts index fe18e46388..91b46961a4 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/coordinates/detail/coordinate-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/coordinates/detail/coordinate-detail.component.ts @@ -20,6 +20,7 @@ import { ResponseCoordinate } from "trafficops-types"; import { CacheGroupService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -34,8 +35,14 @@ export class CoordinateDetailComponent implements OnInit { public new = false; public coordinate!: ResponseCoordinate; - constructor(private readonly route: ActivatedRoute, private readonly cacheGroupService: CacheGroupService, - private readonly location: Location, private readonly dialog: MatDialog, private readonly navSvc: NavigationService) { } + constructor( + private readonly route: ActivatedRoute, + private readonly cacheGroupService: CacheGroupService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly navSvc: NavigationService, + private readonly log: LoggingService, + ) { } /** * Angular lifecycle hook where data is initialized. @@ -43,7 +50,7 @@ export class CoordinateDetailComponent implements OnInit { public async ngOnInit(): Promise { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -61,7 +68,7 @@ export class CoordinateDetailComponent implements OnInit { } const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number:", ID); + this.log.error("route parameter 'id' was non-number:", ID); return; } @@ -74,7 +81,7 @@ export class CoordinateDetailComponent implements OnInit { */ public async deleteCoordinate(): Promise { if (this.new) { - console.error("Unable to delete new coordinate"); + this.log.error("Unable to delete new coordinate"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/cache-groups/divisions/detail/division-detail.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/divisions/detail/division-detail.component.ts index d9d742f0ca..4a9081e598 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/divisions/detail/division-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/divisions/detail/division-detail.component.ts @@ -19,6 +19,7 @@ import { ResponseDivision } from "trafficops-types"; import { CacheGroupService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -32,8 +33,14 @@ export class DivisionDetailComponent implements OnInit { public new = false; public division!: ResponseDivision; - constructor(private readonly route: ActivatedRoute, private readonly cacheGroupService: CacheGroupService, - private readonly location: Location, private readonly dialog: MatDialog, private readonly navSvc: NavigationService) { } + constructor( + private readonly route: ActivatedRoute, + private readonly cacheGroupService: CacheGroupService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly navSvc: NavigationService, + private readonly log: LoggingService, + ) { } /** * Angular lifecycle hook where data is initialized. @@ -41,7 +48,7 @@ export class DivisionDetailComponent implements OnInit { public async ngOnInit(): Promise { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -57,7 +64,7 @@ export class DivisionDetailComponent implements OnInit { } const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number:", ID); + this.log.error("route parameter 'id' was non-number:", ID); return; } @@ -70,7 +77,7 @@ export class DivisionDetailComponent implements OnInit { */ public async deleteDivision(): Promise { if (this.new) { - console.error("Unable to delete new division"); + this.log.error("Unable to delete new division"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/cache-groups/regions/detail/region-detail.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/regions/detail/region-detail.component.ts index c195bd4758..042f7faddf 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/regions/detail/region-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/regions/detail/region-detail.component.ts @@ -19,6 +19,7 @@ import { ResponseDivision, ResponseRegion } from "trafficops-types"; import { CacheGroupService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -34,9 +35,14 @@ export class RegionDetailComponent implements OnInit { public region!: ResponseRegion; public divisions!: Array; - constructor(private readonly route: ActivatedRoute, private readonly cacheGroupService: CacheGroupService, - private readonly location: Location, private readonly dialog: MatDialog, - private readonly header: NavigationService) { + constructor( + private readonly route: ActivatedRoute, + private readonly cacheGroupService: CacheGroupService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly header: NavigationService, + private readonly log: LoggingService, + ) { } /** @@ -46,7 +52,7 @@ export class RegionDetailComponent implements OnInit { this.divisions = await this.cacheGroupService.getDivisions(); const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -64,7 +70,7 @@ export class RegionDetailComponent implements OnInit { } const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number:", ID); + this.log.error("route parameter 'id' was non-number:", ID); return; } @@ -77,7 +83,7 @@ export class RegionDetailComponent implements OnInit { */ public async deleteRegion(): Promise { if (this.new) { - console.error("Unable to delete new region"); + this.log.error("Unable to delete new region"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.ts b/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.ts index 4a31e6277e..e4e966f89d 100644 --- a/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.ts +++ b/experimental/traffic-portal/src/app/core/cache-groups/regions/table/regions-table.component.ts @@ -27,6 +27,7 @@ import type { ContextMenuItem, DoubleClickLink } from "src/app/shared/generic-table/generic-table.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -41,8 +42,14 @@ export class RegionsTableComponent implements OnInit { /** List of regions */ public regions: Promise>; - constructor(private readonly route: ActivatedRoute, private readonly headerSvc: NavigationService, - private readonly api: CacheGroupService, private readonly dialog: MatDialog, public readonly auth: CurrentUserService) { + constructor( + private readonly route: ActivatedRoute, + private readonly headerSvc: NavigationService, + private readonly api: CacheGroupService, + private readonly dialog: MatDialog, + public readonly auth: CurrentUserService, + private readonly log: LoggingService, + ) { this.fuzzySubject = new BehaviorSubject(""); this.regions = this.api.getRegions(); this.headerSvc.headerTitle.next("Regions"); @@ -59,7 +66,7 @@ export class RegionsTableComponent implements OnInit { } }, e => { - console.error("Failed to get query parameters:", e); + this.log.error("Failed to get query parameters:", e); } ); } diff --git a/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.ts b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.ts index fc8d14c480..53fe744732 100644 --- a/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/cdns/cdn-detail/cdn-detail.component.ts @@ -23,6 +23,7 @@ import { DecisionDialogComponent, DecisionDialogData, } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -50,7 +51,8 @@ export class CDNDetailComponent implements OnInit { private readonly api: CDNService, private readonly location: Location, private readonly dialog: MatDialog, - private readonly navSvc: NavigationService + private readonly navSvc: NavigationService, + private readonly log: LoggingService, ) { } @@ -60,7 +62,7 @@ export class CDNDetailComponent implements OnInit { public async ngOnInit(): Promise { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -78,7 +80,7 @@ export class CDNDetailComponent implements OnInit { await cdnsPromise; const index = this.cdns.findIndex(c => c.id === numID); if (index < 0) { - console.error(`no such CDN: #${ID}`); + this.log.error(`no such CDN: #${ID}`); return; } this.cdn = this.cdns.splice(index, 1)[0]; @@ -99,7 +101,7 @@ export class CDNDetailComponent implements OnInit { */ public async delete(): Promise { if (this.new) { - console.error("Unable to delete new CDN"); + this.log.error("Unable to delete new CDN"); return; } const ref = this.dialog.open( diff --git a/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.ts b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.ts index a5d6d07cb0..8bf05e0199 100644 --- a/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.ts +++ b/experimental/traffic-portal/src/app/core/cdns/cdn-table/cdn-table.component.ts @@ -31,6 +31,7 @@ import type { ContextMenuItem, DoubleClickLink } from "src/app/shared/generic-table/generic-table.component"; +import { LoggingService } from "src/app/shared/logging.service"; /** * CDNTableComponent is the controller for the "CDNs" table. @@ -164,6 +165,7 @@ export class CDNTableComponent implements OnInit { public readonly auth: CurrentUserService, private readonly dialog: MatDialog, private readonly route: ActivatedRoute, + private readonly log: LoggingService, ) { this.fuzzySubject = new BehaviorSubject(""); this.cdns = this.api.getCDNs(); @@ -250,27 +252,27 @@ export class CDNTableComponent implements OnInit { switch (a.action) { case "queue": if (Array.isArray(a.data)) { - console.error("cannot queue multiple cdns at once:", a.data); + this.log.error("cannot queue multiple cdns at once:", a.data); return; } this.queueUpdates(a.data); break; case "dequeue": if (Array.isArray(a.data)) { - console.error("cannot dequeue multiple cdns at once:", a.data); + this.log.error("cannot dequeue multiple cdns at once:", a.data); return; } this.queueUpdates(a.data, false); break; case "delete": if (Array.isArray(a.data)) { - console.error("cannot delete multiple cdns at once:", a.data); + this.log.error("cannot delete multiple cdns at once:", a.data); return; } this.delete(a.data); break; default: - console.error("unrecognized context menu action:", a.action); + this.log.error("unrecognized context menu action:", a.action); } } } diff --git a/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.ts b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.ts index 8f7a6c748c..30cdd50c72 100644 --- a/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/certs/cert-detail/cert-detail.component.ts @@ -16,6 +16,7 @@ import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from "@an import { pki, Hex } from "node-forge"; import { oidToName, pkiCertToSHA1, pkiCertToSHA256 } from "src/app/core/certs/cert.util"; +import { LoggingService } from "src/app/shared/logging.service"; /** * Author contains the information about an author from a cert issuer/subject @@ -75,6 +76,8 @@ export class CertDetailComponent implements OnChanges { public sha1: Hex = ""; public sha256: Hex = ""; + constructor(private readonly log: LoggingService) { } + /** * processAttributes converts attributes into an author * @@ -86,7 +89,7 @@ export class CertDetailComponent implements OnChanges { for (const attr of attrs) { if (attr.name && attr.value) { if (typeof attr.value !== "string") { - console.warn(`Unknown attribute value ${attr.value}`); + this.log.warn(`Unknown attribute value ${attr.value}`); continue; } switch (attr.name) { diff --git a/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.ts b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.ts index 0276e73f7c..4f25fbb918 100644 --- a/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.ts +++ b/experimental/traffic-portal/src/app/core/certs/cert-viewer/cert-viewer.component.ts @@ -19,6 +19,7 @@ import { pki } from "node-forge"; import { type ResponseDeliveryServiceSSLKey } from "trafficops-types"; import { DeliveryServiceService } from "src/app/api"; +import { LoggingService } from "src/app/shared/logging.service"; /** * What type of cert is it @@ -63,8 +64,9 @@ export class CertViewerComponent implements OnInit { constructor( private readonly route: ActivatedRoute, private readonly dsAPI: DeliveryServiceService, - private readonly router: Router) { - } + private readonly router: Router, + private readonly log: LoggingService, + ) { } /** * newCert creates a cert from an input string. @@ -77,7 +79,7 @@ export class CertViewerComponent implements OnInit { try { return pki.certificateFromPem(input) as AugmentedCertificate; } catch (e) { - console.error(`ran into issue creating certificate from input ${input}`, e); + this.log.error(`ran into issue creating certificate from input ${input}`, e); return NULL_CERT; } } @@ -143,7 +145,7 @@ export class CertViewerComponent implements OnInit { rootFirst = false; } else { invalid = true; - console.error(`Cert chain is invalid, cert ${i-1} and ${i} are not related`); + this.log.error(`Cert chain is invalid, cert ${i-1} and ${i} are not related`); } } diff --git a/experimental/traffic-portal/src/app/core/core.module.ts b/experimental/traffic-portal/src/app/core/core.module.ts index acc912ae87..4a5f1a96ad 100644 --- a/experimental/traffic-portal/src/app/core/core.module.ts +++ b/experimental/traffic-portal/src/app/core/core.module.ts @@ -26,8 +26,8 @@ import { AppUIModule } from "../app.ui.module"; import { AuthenticatedGuard } from "../guards/authenticated-guard.service"; import { SharedModule } from "../shared/shared.module"; -import { AsnDetailComponent } from "./cache-groups/asns/detail/asn-detail.component"; -import { AsnsTableComponent } from "./cache-groups/asns/table/asns-table.component"; +import { ASNDetailComponent } from "./cache-groups/asns/detail/asn-detail.component"; +import { ASNsTableComponent } from "./cache-groups/asns/table/asns-table.component"; import { CacheGroupDetailsComponent } from "./cache-groups/cache-group-details/cache-group-details.component"; import { CacheGroupTableComponent } from "./cache-groups/cache-group-table/cache-group-table.component"; import { CoordinateDetailComponent } from "./cache-groups/coordinates/detail/coordinate-detail.component"; @@ -85,8 +85,8 @@ export const ROUTES: Routes = [ path: "certs" }, { component: DashboardComponent, path: "" }, - { component: AsnDetailComponent, path: "asns/:id"}, - { component: AsnsTableComponent, path: "asns" }, + { component: ASNDetailComponent, path: "asns/:id"}, + { component: ASNsTableComponent, path: "asns" }, { component: DivisionsTableComponent, path: "divisions" }, { component: DivisionDetailComponent, path: "divisions/:id" }, { component: RegionsTableComponent, path: "regions" }, @@ -134,53 +134,52 @@ export const ROUTES: Routes = [ */ @NgModule({ declarations: [ - UsersComponent, - ServerDetailsComponent, - ServersTableComponent, - DeliveryserviceComponent, - NewDeliveryServiceComponent, + ASNDetailComponent, + ASNsTableComponent, + CacheGroupDetailsComponent, + CacheGroupTableComponent, + CapabilitiesComponent, + CapabilityDetailsComponent, + CDNDetailComponent, + CDNTableComponent, + ChangeLogsComponent, + CoordinateDetailComponent, + CoordinatesTableComponent, CurrentuserComponent, - UpdatePasswordDialogComponent, DashboardComponent, + DeliveryserviceComponent, + DivisionDetailComponent, + DivisionsTableComponent, DsCardComponent, InvalidationJobsComponent, - CacheGroupTableComponent, - NewInvalidationJobDialogComponent, - UpdateStatusComponent, - UserDetailsComponent, - TenantsComponent, - UserRegistrationDialogComponent, - RolesTableComponent, - RoleDetailComponent, - TenantDetailsComponent, - ChangeLogsComponent, + ISOGenerationFormComponent, LastDaysComponent, - UserRegistrationDialogComponent, - PhysLocTableComponent, + NewDeliveryServiceComponent, + NewInvalidationJobDialogComponent, + ParameterDetailComponent, + ParametersTableComponent, PhysLocDetailComponent, - AsnsTableComponent, - AsnDetailComponent, - DivisionsTableComponent, - DivisionDetailComponent, - RegionsTableComponent, + PhysLocTableComponent, + ProfileDetailComponent, + ProfileTableComponent, RegionDetailComponent, - CacheGroupDetailsComponent, - TypesTableComponent, - TypeDetailComponent, - CoordinatesTableComponent, - CoordinateDetailComponent, - StatusesTableComponent, + RegionsTableComponent, + RoleDetailComponent, + RolesTableComponent, + ServerDetailsComponent, + ServersTableComponent, StatusDetailsComponent, - ISOGenerationFormComponent, - CDNTableComponent, - CDNDetailComponent, - ParametersTableComponent, - ParameterDetailComponent, - ProfileTableComponent, - ProfileDetailComponent, - CapabilitiesComponent, - CapabilityDetailsComponent, + StatusesTableComponent, + TenantDetailsComponent, + TenantsComponent, TopologyDetailsComponent, + TypeDetailComponent, + TypesTableComponent, + UpdatePasswordDialogComponent, + UpdateStatusComponent, + UserDetailsComponent, + UserRegistrationDialogComponent, + UsersComponent, ], exports: [], imports: [ diff --git a/experimental/traffic-portal/src/app/core/currentuser/currentuser.component.ts b/experimental/traffic-portal/src/app/core/currentuser/currentuser.component.ts index 3a1d06995a..00ddec2d29 100644 --- a/experimental/traffic-portal/src/app/core/currentuser/currentuser.component.ts +++ b/experimental/traffic-portal/src/app/core/currentuser/currentuser.component.ts @@ -18,6 +18,7 @@ import { ResponseCurrentUser } from "trafficops-types"; import { UserService } from "src/app/api"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; import {ThemeManagerService} from "src/app/shared/theme-manager/theme-manager.service"; @@ -54,7 +55,8 @@ export class CurrentuserComponent implements OnInit { private readonly route: ActivatedRoute, private readonly router: Router, private readonly navSvc: NavigationService, - public readonly themeSvc: ThemeManagerService + public readonly themeSvc: ThemeManagerService, + private readonly log: LoggingService ) { this.currentUser = this.auth.currentUser; } @@ -141,12 +143,12 @@ export class CurrentuserComponent implements OnInit { if (success) { const updated = await this.auth.updateCurrentUser(); if (!updated) { - console.warn("Failed to fetch current user after successful update"); + this.log.warn("Failed to fetch current user after successful update"); } this.currentUser = this.auth.currentUser; this.cancelEdit(); } else { - console.warn("Editing the current user failed"); + this.log.warn("Editing the current user failed"); this.cancelEdit(); } } diff --git a/experimental/traffic-portal/src/app/core/currentuser/update-password-dialog/update-password-dialog.component.ts b/experimental/traffic-portal/src/app/core/currentuser/update-password-dialog/update-password-dialog.component.ts index d524b76c15..3923269044 100644 --- a/experimental/traffic-portal/src/app/core/currentuser/update-password-dialog/update-password-dialog.component.ts +++ b/experimental/traffic-portal/src/app/core/currentuser/update-password-dialog/update-password-dialog.component.ts @@ -16,6 +16,7 @@ import { MatDialogRef } from "@angular/material/dialog"; import { Subject } from "rxjs"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; +import { LoggingService } from "src/app/shared/logging.service"; /** * This is the controller for the "Update Password" dialog box/form. @@ -36,8 +37,9 @@ export class UpdatePasswordDialogComponent { constructor( private readonly dialog: MatDialogRef, - private readonly auth: CurrentUserService - ) { } + private readonly auth: CurrentUserService, + private readonly log: LoggingService, + ) {} /** * Cancels the password update, closing the dialog box. @@ -63,7 +65,7 @@ export class UpdatePasswordDialogComponent { } if (!this.auth.currentUser) { - console.error("Cannot update null user"); + this.log.error("Cannot update null user"); return; } diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.ts b/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.ts index 676d0e230d..51d3e65e7a 100644 --- a/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.ts +++ b/experimental/traffic-portal/src/app/core/deliveryservice/deliveryservice.component.ts @@ -20,6 +20,7 @@ import { AlertLevel, ResponseDeliveryService } from "trafficops-types"; import { DeliveryServiceService } from "src/app/api"; import type { DataPoint, DataSet } from "src/app/models"; import { AlertService } from "src/app/shared/alert/alert.service"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -77,14 +78,12 @@ export class DeliveryserviceComponent implements OnInit { /** The size of each single interval for data grouping, in seconds. */ private bucketSize = 300; - /** - * Constructor. - */ constructor( private readonly route: ActivatedRoute, private readonly api: DeliveryServiceService, private readonly alerts: AlertService, - private readonly navSvc: NavigationService + private readonly navSvc: NavigationService, + private readonly log: LoggingService, ) { this.bandwidthData.next([{ backgroundColor: "#BA3C57", @@ -116,7 +115,7 @@ export class DeliveryserviceComponent implements OnInit { const DSID = this.route.snapshot.paramMap.get("id"); if (!DSID) { - console.error("Missing route 'id' parameter"); + this.log.error("Missing route 'id' parameter"); return; } @@ -185,7 +184,7 @@ export class DeliveryserviceComponent implements OnInit { data = await this.api.getDSKBPS(xmlID, this.from, this.to, interval, false, true); } catch (e) { this.alerts.newAlert(AlertLevel.WARNING, "Edge-Tier bandwidth data not found!"); - console.error(`Failed to get edge KBPS data for '${xmlID}':`, e); + this.log.error(`Failed to get edge KBPS data for '${xmlID}':`, e); return; } @@ -232,7 +231,7 @@ export class DeliveryserviceComponent implements OnInit { ]); }, e => { - console.error(`Failed to get edge TPS data for '${this.deliveryservice.xmlId}':`, e); + this.log.error(`Failed to get edge TPS data for '${this.deliveryservice.xmlId}':`, e); this.alerts.newAlert(AlertLevel.WARNING, "Edge-Tier transaction data not found!"); } ); diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/ds-card/ds-card.component.ts b/experimental/traffic-portal/src/app/core/deliveryservice/ds-card/ds-card.component.ts index c10ac704f6..cbf2481afd 100644 --- a/experimental/traffic-portal/src/app/core/deliveryservice/ds-card/ds-card.component.ts +++ b/experimental/traffic-portal/src/app/core/deliveryservice/ds-card/ds-card.component.ts @@ -21,6 +21,7 @@ import type { DataPoint, DataSet, } from "src/app/models"; +import { LoggingService } from "src/app/shared/logging.service"; /** * DsCardComponent is a component for displaying information about a Delivery @@ -118,7 +119,7 @@ export class DsCardComponent implements OnInit { return ""; } - constructor(private readonly dsAPI: DeliveryServiceService) { + constructor(private readonly dsAPI: DeliveryServiceService, private readonly log: LoggingService) { this.available = 100; this.maintenance = 0; this.utilized = 0; @@ -151,10 +152,6 @@ export class DsCardComponent implements OnInit { * 00:00 UTC the current day and ending at the current date/time. */ public toggle(): void { - if (!this.deliveryService.id) { - console.error("Toggling DS card for DS with no ID:", this.deliveryService); - return; - } if (!this.open) { if (!this.loaded) { this.loaded = true; @@ -226,7 +223,7 @@ export class DsCardComponent implements OnInit { fillColor: "#3CBA9F", label: "Edge-tier Bandwidth" }]); - console.error(`Failed getting edge KBPS for DS '${xmlID}':`, e); + this.log.error(`Failed getting edge KBPS for DS '${xmlID}':`, e); } } } diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/invalidation-jobs.component.ts b/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/invalidation-jobs.component.ts index ccef99e4b6..0480879fc6 100644 --- a/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/invalidation-jobs.component.ts +++ b/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/invalidation-jobs.component.ts @@ -17,6 +17,7 @@ import { ActivatedRoute } from "@angular/router"; import { ResponseDeliveryService, ResponseInvalidationJob } from "trafficops-types"; import { DeliveryServiceService, InvalidationJobService } from "src/app/api"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; import { @@ -51,21 +52,22 @@ export class InvalidationJobsComponent implements OnInit { private readonly jobAPI: InvalidationJobService, private readonly dsAPI: DeliveryServiceService, private readonly dialog: MatDialog, - private readonly navSvc: NavigationService + private readonly navSvc: NavigationService, + private readonly log: LoggingService, ) { this.jobs = new Array(); } /** * Runs initialization, fetching the jobs and Delivery Service data from - * Traffic Ops and setting the pageload date/time. + * Traffic Ops and setting the date/time on page load. */ public async ngOnInit(): Promise { this.navSvc.headerTitle.next("Loading - Content Invalidation Jobs"); this.now = new Date(); const idParam = this.route.snapshot.paramMap.get("id"); if (!idParam) { - console.error("Missing route 'id' parameter"); + this.log.error("Missing route 'id' parameter"); return; } this.dsID = parseInt(idParam, 10); diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts b/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts index fdb768b699..8bf52574e7 100644 --- a/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts +++ b/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts @@ -25,14 +25,14 @@ describe("NewInvalidationJobDialogComponent", () => { let component: NewInvalidationJobDialogComponent; let fixture: ComponentFixture; const dialogRef = { - close: jasmine.createSpy("dialog 'close' method", (): void => console.log("dialog closed")) + close: jasmine.createSpy("dialog 'close' method", (): void => { /* Do nothing */ }) }; const dialogData = { dsID: -1 }; beforeEach(async () => { - dialogRef.close = jasmine.createSpy("dialog 'close' method", (): void => console.log("dialog closed")); + dialogRef.close = jasmine.createSpy("dialog 'close' method", (): void => { /* Do nothing */ }); await TestBed.configureTestingModule({ declarations: [ NewInvalidationJobDialogComponent ], imports: [ @@ -128,7 +128,7 @@ describe("NewInvalidationJobDialogComponent - editing", () => { let component: NewInvalidationJobDialogComponent; let fixture: ComponentFixture; const dialogRef = { - close: jasmine.createSpy("dialog 'close' method", (): void => console.log("dialog closed")) + close: jasmine.createSpy("dialog 'close' method", (): void => { /* Do nothing */ }) }; const dialogData = { dsID: -1, @@ -142,7 +142,7 @@ describe("NewInvalidationJobDialogComponent - editing", () => { }; beforeEach(async () => { - dialogRef.close = jasmine.createSpy("dialog 'close' method", (): void => console.log("dialog closed")); + dialogRef.close = jasmine.createSpy("dialog 'close' method", (): void => { /* Do nothing */ }); await TestBed.configureTestingModule({ declarations: [ NewInvalidationJobDialogComponent ], imports: [ diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts b/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts index 6102ba30ed..048f9b29cb 100644 --- a/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts +++ b/experimental/traffic-portal/src/app/core/deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts @@ -18,6 +18,7 @@ import { Subject } from "rxjs"; import { JobType, ResponseInvalidationJob } from "trafficops-types"; import { InvalidationJobService } from "src/app/api"; +import { LoggingService } from "src/app/shared/logging.service"; /** * Gets the time part of a Date as a string. @@ -95,7 +96,8 @@ export class NewInvalidationJobDialogComponent { constructor( private readonly dialogRef: MatDialogRef, private readonly jobAPI: InvalidationJobService, - @Inject(MAT_DIALOG_DATA) data: NewInvalidationJobDialogData + @Inject(MAT_DIALOG_DATA) data: NewInvalidationJobDialogData, + private readonly log: LoggingService, ) { this.job = data.job; if (this.job) { @@ -152,7 +154,7 @@ export class NewInvalidationJobDialogComponent { await this.jobAPI.updateInvalidationJob(job); this.dialogRef.close(true); } catch (e) { - console.error("error:", e); + this.log.error(`failed to edit Job #${j.id}:`, e); } } @@ -193,7 +195,7 @@ export class NewInvalidationJobDialogComponent { await this.jobAPI.createInvalidationJob(job); this.dialogRef.close(true); } catch (err) { - console.error("error: ", err); + this.log.error("failed to create invalidation job: ", err); } } diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.spec.ts b/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.spec.ts index dfed761f47..429b7c1c76 100644 --- a/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.spec.ts +++ b/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.spec.ts @@ -110,27 +110,23 @@ describe("NewDeliveryServiceComponent", () => { }); it("should set meta info properly", async () => { - try { - const stepper = await loader.getHarness(MatStepperHarness); - const steps = await stepper.getSteps(); - await steps[1].select(); - component.displayName.setValue("test._QUEST"); - component.infoURL.setValue("ftp://this-is-a-weird.url/"); - component.description.setValue("test description"); - component.setMetaInformation(); - - expect(component.deliveryService.displayName).toEqual("test._QUEST", "test._QUEST"); - expect(component.deliveryService.xmlId).toEqual("test-quest", "test-quest"); - expect(component.deliveryService.longDesc).toEqual("test description", "test description"); - expect(component.deliveryService.infoUrl).toEqual("ftp://this-is-a-weird.url/", "ftp://this-is-a-weird.url/"); - expect(await parallel(() => steps.map(async step => step.isSelected()))).toEqual([ - false, - false, - true - ]); - } catch (e) { - console.error("Error occurred:", e); - } + const stepper = await loader.getHarness(MatStepperHarness); + const steps = await stepper.getSteps(); + await steps[1].select(); + component.displayName.setValue("test._QUEST"); + component.infoURL.setValue("ftp://this-is-a-weird.url/"); + component.description.setValue("test description"); + component.setMetaInformation(); + + expect(component.deliveryService.displayName).toEqual("test._QUEST", "test._QUEST"); + expect(component.deliveryService.xmlId).toEqual("test-quest", "test-quest"); + expect(component.deliveryService.longDesc).toEqual("test description", "test description"); + expect(component.deliveryService.infoUrl).toEqual("ftp://this-is-a-weird.url/", "ftp://this-is-a-weird.url/"); + expect(await parallel(() => steps.map(async step => step.isSelected()))).toEqual([ + false, + false, + true + ]); }); it("should set infrastructure info properly for HTTP Delivery Services", () => { diff --git a/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.ts b/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.ts index a8882a6d1b..828164dbf7 100644 --- a/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.ts +++ b/experimental/traffic-portal/src/app/core/deliveryservice/new-delivery-service/new-delivery-service.component.ts @@ -31,6 +31,7 @@ import { import { CDNService, DeliveryServiceService } from "src/app/api"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; import { IPV4, IPV6 } from "src/app/utils"; @@ -145,7 +146,8 @@ export class NewDeliveryServiceComponent implements OnInit { private readonly auth: CurrentUserService, private readonly router: Router, private readonly navSvc: NavigationService, - @Inject(DOCUMENT) private readonly document: Document + @Inject(DOCUMENT) private readonly document: Document, + private readonly log: LoggingService, ) { } /** @@ -173,7 +175,7 @@ export class NewDeliveryServiceComponent implements OnInit { } ); if (!this.auth.currentUser || !this.auth.currentUser.tenantId) { - console.error("Cannot set default CDN - user has no tenant"); + this.log.error("Cannot set default CDN - user has no tenant"); return typeP; } const dsP = this.dsAPI.getDeliveryServices().then( @@ -261,7 +263,7 @@ export class NewDeliveryServiceComponent implements OnInit { try { url = new URL(this.originURL.value ?? ""); } catch (e) { - console.error("invalid origin URL:", e); + this.log.error("invalid origin URL:", e); return; } this.deliveryService.orgServerFqdn = url.origin; @@ -353,7 +355,7 @@ export class NewDeliveryServiceComponent implements OnInit { try { this.setDNSBypass(this.bypassLoc.value ?? ""); } catch (e) { - console.error(e); + this.log.error("failed to set DNS bypass:", e); const nativeBypassElement = this.document.getElementById("bypass-loc") as HTMLInputElement; nativeBypassElement.setCustomValidity(e instanceof Error ? e.message : String(e)); nativeBypassElement.reportValidity(); @@ -370,7 +372,6 @@ export class NewDeliveryServiceComponent implements OnInit { this.dsAPI.createDeliveryService(this.deliveryService).then( v => { - console.log("New Delivery Service '%s' created", v.displayName); this.router.navigate(["/"], {queryParams: {search: encodeURIComponent(v.displayName)}}); } ); diff --git a/experimental/traffic-portal/src/app/core/parameters/detail/parameter-detail.component.ts b/experimental/traffic-portal/src/app/core/parameters/detail/parameter-detail.component.ts index cc79127ecb..a78d4e11a8 100644 --- a/experimental/traffic-portal/src/app/core/parameters/detail/parameter-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/parameters/detail/parameter-detail.component.ts @@ -20,6 +20,7 @@ import { ResponseParameter } from "trafficops-types"; import { ProfileService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -38,8 +39,14 @@ export class ParameterDetailComponent implements OnInit { { label: "false", value: false } ]; - constructor(private readonly route: ActivatedRoute, private readonly profileService: ProfileService, - private readonly location: Location, private readonly dialog: MatDialog, private readonly navSvc: NavigationService) { } + constructor( + private readonly route: ActivatedRoute, + private readonly profileService: ProfileService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly navSvc: NavigationService, + private readonly log: LoggingService, + ) { } /** * Angular lifecycle hook where data is initialized. @@ -47,7 +54,7 @@ export class ParameterDetailComponent implements OnInit { public async ngOnInit(): Promise { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -68,7 +75,7 @@ export class ParameterDetailComponent implements OnInit { const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number: ", ID); + this.log.error("route parameter 'id' was non-number: ", ID); return; } @@ -81,7 +88,7 @@ export class ParameterDetailComponent implements OnInit { */ public async deleteParameter(): Promise { if (this.new) { - console.error("Unable to delete new parameter"); + this.log.error("Unable to delete new parameter"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.ts b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.ts index 78e0c362ff..cfc4381399 100644 --- a/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/profiles/profile-detail/profile-detail.component.ts @@ -19,6 +19,7 @@ import { ProfileType, ResponseCDN, ResponseProfile } from "trafficops-types"; import { CDNService, ProfileService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -59,23 +60,14 @@ export class ProfileDetailComponent implements OnInit { { value: "GROVE_PROFILE" } ]; - /** - * Constructor. - * - * @param api The Profiles API which is used to provide functions for create, edit and delete profiles. - * @param cdnService The CDN service API which is used to provide cdns. - * @param dialog Dialog manager - * @param navSvc Manages the header - * @param route A reference to the route of this view which is used to get the 'id' query parameter of profile. - * @param router Angular router - */ constructor( private readonly api: ProfileService, private readonly cdnService: CDNService, private readonly dialog: MatDialog, private readonly navSvc: NavigationService, private readonly route: ActivatedRoute, - private readonly router: Router + private readonly router: Router, + private readonly log: LoggingService, ) { } /** @@ -133,7 +125,7 @@ export class ProfileDetailComponent implements OnInit { */ public async deleteProfile(): Promise { if (this.new) { - console.error("Unable to delete new profile"); + this.log.error("Unable to delete new profile"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/servers/capabilities/capabilities.component.ts b/experimental/traffic-portal/src/app/core/servers/capabilities/capabilities.component.ts index 5fe946ec49..88a0e30aa5 100644 --- a/experimental/traffic-portal/src/app/core/servers/capabilities/capabilities.component.ts +++ b/experimental/traffic-portal/src/app/core/servers/capabilities/capabilities.component.ts @@ -26,6 +26,7 @@ import type { ContextMenuItem, DoubleClickLink } from "src/app/shared/generic-table/generic-table.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -124,7 +125,8 @@ export class CapabilitiesComponent implements OnInit { private readonly route: ActivatedRoute, private readonly navSvc: NavigationService, private readonly dialog: MatDialog, - public readonly auth: CurrentUserService + public readonly auth: CurrentUserService, + private readonly log: LoggingService ) { this.fuzzySubject = new BehaviorSubject(""); this.capabilities = this.api.getCapabilities(); @@ -173,7 +175,7 @@ export class CapabilitiesComponent implements OnInit { } break; default: - console.warn("unrecognized context menu action:", evt.action); + this.log.warn("unrecognized context menu action:", evt.action); } } } diff --git a/experimental/traffic-portal/src/app/core/servers/phys-loc/detail/phys-loc-detail.component.ts b/experimental/traffic-portal/src/app/core/servers/phys-loc/detail/phys-loc-detail.component.ts index b93e84ccbb..51b9461615 100644 --- a/experimental/traffic-portal/src/app/core/servers/phys-loc/detail/phys-loc-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/servers/phys-loc/detail/phys-loc-detail.component.ts @@ -19,6 +19,7 @@ import { ResponsePhysicalLocation, ResponseRegion } from "trafficops-types"; import { CacheGroupService, PhysicalLocationService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -35,10 +36,15 @@ export class PhysLocDetailComponent implements OnInit { public physLocation!: ResponsePhysicalLocation; public regions!: Array; - constructor(private readonly route: ActivatedRoute, private readonly cacheGroupService: CacheGroupService, - private readonly location: Location, private readonly dialog: MatDialog, private readonly navSvc: NavigationService, - private readonly physLocService: PhysicalLocationService) { - } + constructor( + private readonly route: ActivatedRoute, + private readonly cacheGroupService: CacheGroupService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly navSvc: NavigationService, + private readonly physLocService: PhysicalLocationService, + private readonly log: LoggingService, + ) { } /** * Angular lifecycle hook. @@ -47,7 +53,7 @@ export class PhysLocDetailComponent implements OnInit { this.regions = await this.cacheGroupService.getRegions(); const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -74,7 +80,7 @@ export class PhysLocDetailComponent implements OnInit { } const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number:", ID); + this.log.error("route parameter 'id' was non-number:", ID); return; } @@ -87,7 +93,7 @@ export class PhysLocDetailComponent implements OnInit { */ public async deletePhysicalLocation(): Promise { if (this.new) { - console.error("Unable to delete new physLocation"); + this.log.error("Unable to delete new physLocation"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts index 3f78be5141..9b1f25b139 100644 --- a/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts +++ b/experimental/traffic-portal/src/app/core/servers/server-details/server-details.component.ts @@ -34,6 +34,7 @@ import { DecisionDialogComponent, DecisionDialogData } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; import { IP, IP_WITH_CIDR, AutocompleteValue } from "src/app/utils"; @@ -118,7 +119,8 @@ export class ServerDetailsComponent implements OnInit { private readonly typeService: TypeService, private readonly physlocService: PhysicalLocationService, private readonly navSvc: NavigationService, - private readonly dialog: MatDialog + private readonly dialog: MatDialog, + private readonly log: LoggingService, ) { } @@ -128,7 +130,7 @@ export class ServerDetailsComponent implements OnInit { public ngOnInit(): void { const handleErr = (obj: string): (e: unknown) => void => (e: unknown): void => { - console.error(`Failed to get ${obj}:`, e); + this.log.error(`Failed to get ${obj}:`, e); }; this.cacheGroupService.getCacheGroups().then( @@ -164,7 +166,7 @@ export class ServerDetailsComponent implements OnInit { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -178,7 +180,7 @@ export class ServerDetailsComponent implements OnInit { } ).catch( e => { - console.error(`Failed to get server #${ID}:`, e); + this.log.error(`Failed to get server #${ID}:`, e); } ); } else { @@ -265,7 +267,7 @@ export class ServerDetailsComponent implements OnInit { this.router.navigate(["server", s.id]); }, err => { - console.error("failed to create server:", err); + this.log.error("failed to create server:", err); } ); } else { @@ -275,7 +277,7 @@ export class ServerDetailsComponent implements OnInit { this.updateTitlebar(); }, err => { - console.error(`failed to update server: ${err}`); + this.log.error(`failed to update server: ${err}`); } ); } @@ -285,7 +287,7 @@ export class ServerDetailsComponent implements OnInit { */ public delete(): void { if (this.isNew) { - console.error("Unable to delete new Cache Group"); + this.log.error("Unable to delete new Cache Group"); return; } const ref = this.dialog.open( @@ -324,7 +326,7 @@ export class ServerDetailsComponent implements OnInit { } }, err => { - console.error(`failed to queue updates: ${err}`); + this.log.error(`failed to queue updates: ${err}`); }); } @@ -338,7 +340,7 @@ export class ServerDetailsComponent implements OnInit { } }, err => { - console.error(`failed to dequeue updates: ${err}`); + this.log.error(`failed to dequeue updates: ${err}`); }); } @@ -450,7 +452,7 @@ export class ServerDetailsComponent implements OnInit { s => this.server = s ).catch( err => { - console.error("Failed to reload servers:", err); + this.log.error("Failed to reload servers:", err); } ); } diff --git a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts index 4f8d0627b7..57c89bb31c 100644 --- a/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts +++ b/experimental/traffic-portal/src/app/core/servers/update-status/update-status.component.ts @@ -16,6 +16,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import type { ResponseServer, ResponseStatus } from "trafficops-types"; import { ServerService } from "src/app/api/server.service"; +import { LoggingService } from "src/app/shared/logging.service"; /** * UpdateStatusComponent is the controller for the "Update Server Status" dialog box. @@ -53,10 +54,12 @@ export class UpdateStatusComponent implements OnInit { return `${len} servers`; } - /** Constructor. */ - constructor(private readonly dialogRef: MatDialogRef, + constructor( + private readonly dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) private readonly dialogServers: Array, - private readonly api: ServerService) { + private readonly api: ServerService, + private readonly log: LoggingService, + ) { this.servers = this.dialogServers; } @@ -70,7 +73,7 @@ export class UpdateStatusComponent implements OnInit { } ).catch( e => { - console.error("Failed to get Statuses:", e); + this.log.error("Failed to get Statuses:", e); } ); if (this.servers.length < 1) { @@ -110,7 +113,7 @@ export class UpdateStatusComponent implements OnInit { await Promise.all(observables); this.dialogRef.close(true); } catch (err) { - console.error("something went wrong trying to update", this.serverName, "servers:", err); + this.log.error("something went wrong trying to update", this.serverName, "servers:", err); this.dialogRef.close(false); } } diff --git a/experimental/traffic-portal/src/app/core/topologies/topology-details/topology-details.component.ts b/experimental/traffic-portal/src/app/core/topologies/topology-details/topology-details.component.ts index 81089fedd1..de41c41781 100644 --- a/experimental/traffic-portal/src/app/core/topologies/topology-details/topology-details.component.ts +++ b/experimental/traffic-portal/src/app/core/topologies/topology-details/topology-details.component.ts @@ -24,9 +24,8 @@ import { DecisionDialogComponent, DecisionDialogData, } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; -import { - NavigationService -} from "src/app/shared/navigation/navigation.service"; +import { LoggingService } from "src/app/shared/logging.service"; +import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** * TopologyDetailComponent is the controller for a Topology's "detail" page. @@ -61,8 +60,8 @@ export class TopologyDetailsComponent implements OnInit { private readonly location: Location, private readonly dialog: MatDialog, private readonly navSvc: NavigationService, - ) { - } + private readonly log: LoggingService, + ) { } /** * Angular lifecycle hook where data is initialized. @@ -81,7 +80,7 @@ export class TopologyDetailsComponent implements OnInit { await topologiesPromise; const index = this.topologies.findIndex(c => c.name === name); if (index < 0) { - console.error(`no such Topology: ${name}`); + this.log.error(`no such Topology: ${name}`); this.loading = false; return; } @@ -116,7 +115,7 @@ export class TopologyDetailsComponent implements OnInit { */ public async delete(): Promise { if (this.new) { - console.error("Unable to delete new Topology"); + this.log.error("Unable to delete new Topology"); return; } const ref = this.dialog.open( diff --git a/experimental/traffic-portal/src/app/core/types/detail/type-detail.component.ts b/experimental/traffic-portal/src/app/core/types/detail/type-detail.component.ts index 2a88f7913f..ef1ec52507 100644 --- a/experimental/traffic-portal/src/app/core/types/detail/type-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/types/detail/type-detail.component.ts @@ -20,6 +20,7 @@ import { TypeFromResponse } from "trafficops-types"; import { TypeService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -34,8 +35,14 @@ export class TypeDetailComponent implements OnInit { public new = false; public type!: TypeFromResponse; - constructor(private readonly route: ActivatedRoute, private readonly typeService: TypeService, - private readonly location: Location, private readonly dialog: MatDialog, private readonly navSvc: NavigationService) { } + constructor( + private readonly route: ActivatedRoute, + private readonly typeService: TypeService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly navSvc: NavigationService, + private readonly log: LoggingService, + ) { } /** * Angular lifecycle hook where data is initialized. @@ -43,7 +50,7 @@ export class TypeDetailComponent implements OnInit { public async ngOnInit(): Promise { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -62,7 +69,7 @@ export class TypeDetailComponent implements OnInit { const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number: ", ID); + this.log.error("route parameter 'id' was non-number: ", ID); return; } @@ -75,7 +82,7 @@ export class TypeDetailComponent implements OnInit { */ public async deleteType(): Promise { if (this.new) { - console.error("Unable to delete new type"); + this.log.error("Unable to delete new type"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts index 2454abae81..c6203eeccf 100644 --- a/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts +++ b/experimental/traffic-portal/src/app/core/users/roles/detail/role-detail.component.ts @@ -19,6 +19,7 @@ import { ResponseRole } from "trafficops-types"; import { UserService } from "src/app/api"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -39,10 +40,15 @@ export class RoleDetailComponent implements OnInit { */ private name = ""; - constructor(private readonly route: ActivatedRoute, private readonly router: Router, - private readonly userService: UserService, private readonly location: Location, - private readonly dialog: MatDialog, private readonly header: NavigationService) { - } + constructor( + private readonly route: ActivatedRoute, + private readonly router: Router, + private readonly userService: UserService, + private readonly location: Location, + private readonly dialog: MatDialog, + private readonly header: NavigationService, + private readonly log: LoggingService, + ) { } /** * Angular lifecycle hook where data is initialized. @@ -82,7 +88,7 @@ export class RoleDetailComponent implements OnInit { */ public async deleteRole(): Promise { if (this.new) { - console.error("Unable to delete new role"); + this.log.error("Unable to delete new role"); return; } const ref = this.dialog.open(DecisionDialogComponent, { diff --git a/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts index 5e66bff8bc..dfc9228aa5 100644 --- a/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts +++ b/experimental/traffic-portal/src/app/core/users/roles/table/roles-table.component.ts @@ -23,6 +23,7 @@ import { UserService } from "src/app/api"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component"; import type { ContextMenuActionEvent, ContextMenuItem, DoubleClickLink } from "src/app/shared/generic-table/generic-table.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -36,8 +37,15 @@ import { NavigationService } from "src/app/shared/navigation/navigation.service" export class RolesTableComponent implements OnInit { /** List of roles */ public roles: Promise>; - constructor(private readonly route: ActivatedRoute, private readonly headerSvc: NavigationService, - private readonly api: UserService, private readonly dialog: MatDialog, public readonly auth: CurrentUserService) { + + constructor( + private readonly route: ActivatedRoute, + private readonly headerSvc: NavigationService, + private readonly api: UserService, + private readonly dialog: MatDialog, + public readonly auth: CurrentUserService, + private readonly log: LoggingService, + ) { this.fuzzySubject = new BehaviorSubject(""); this.roles = this.api.getRoles(); this.headerSvc.headerTitle.next("Roles"); @@ -54,7 +62,7 @@ export class RolesTableComponent implements OnInit { } }, e => { - console.error("Failed to get query parameters:", e); + this.log.error("Failed to get query parameters:", e); } ); } @@ -123,7 +131,7 @@ export class RolesTableComponent implements OnInit { */ public async handleContextMenu(evt: ContextMenuActionEvent): Promise { if (Array.isArray(evt.data)) { - console.error("cannot delete multiple roles at once:", evt.data); + this.log.error("cannot delete multiple roles at once:", evt.data); return; } const data = evt.data; diff --git a/experimental/traffic-portal/src/app/core/users/tenants/tenant-details/tenant-details.component.ts b/experimental/traffic-portal/src/app/core/users/tenants/tenant-details/tenant-details.component.ts index 89b0dc593b..43d9cb8e0b 100644 --- a/experimental/traffic-portal/src/app/core/users/tenants/tenant-details/tenant-details.component.ts +++ b/experimental/traffic-portal/src/app/core/users/tenants/tenant-details/tenant-details.component.ts @@ -18,6 +18,7 @@ import { RequestTenant, ResponseTenant, Tenant } from "trafficops-types"; import { UserService } from "src/app/api"; import { TreeData } from "src/app/models"; +import { LoggingService } from "src/app/shared/logging.service"; /** * TenantsDetailsComponent is the controller for the tenant add/edit form. @@ -33,8 +34,12 @@ export class TenantDetailsComponent implements OnInit { public tenants = new Array(); public displayTenant: TreeData; - constructor(private readonly route: ActivatedRoute, private readonly userService: UserService, - private readonly location: Location) { + constructor( + private readonly route: ActivatedRoute, + private readonly userService: UserService, + private readonly location: Location, + private readonly log: LoggingService, + ) { this.displayTenant = { children: [], id: -1, @@ -50,7 +55,7 @@ export class TenantDetailsComponent implements OnInit { public update(evt: TreeData): void { const tenant = this.tenants.find(t => t.id === evt.id); if (tenant === undefined) { - console.error(`Unknown tenant selected ${evt.id}`); + this.log.error(`Unknown tenant selected ${evt.id}`); return; } this.tenant.parentId = tenant.id; @@ -102,7 +107,7 @@ export class TenantDetailsComponent implements OnInit { public async ngOnInit(): Promise { const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } @@ -119,12 +124,12 @@ export class TenantDetailsComponent implements OnInit { } const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number:", ID); + this.log.error("route parameter 'id' was non-number:", ID); return; } const tenant = this.tenants.find(t => t.id === numID); if (!tenant) { - console.error(`Unable to find tenant with id ${numID}`); + this.log.error(`Unable to find tenant with id ${numID}`); return; } this.tenant = tenant; @@ -155,7 +160,7 @@ export class TenantDetailsComponent implements OnInit { */ public async deleteTenant(): Promise { if (this.new) { - console.error("Unable to delete new tenant"); + this.log.error("Unable to delete new tenant"); return; } await this.userService.deleteTenant((this.tenant as ResponseTenant).id); diff --git a/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.ts b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.ts index 2a73e5d880..a30216c66e 100644 --- a/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.ts +++ b/experimental/traffic-portal/src/app/core/users/tenants/tenants.component.ts @@ -25,6 +25,7 @@ import type { ContextMenuItem, DoubleClickLink } from "src/app/shared/generic-table/generic-table.component"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; /** @@ -96,7 +97,8 @@ export class TenantsComponent implements OnInit, OnDestroy { constructor( private readonly userService: UserService, public readonly auth: CurrentUserService, - private readonly navSvc: NavigationService + private readonly navSvc: NavigationService, + private readonly log: LoggingService, ) { this.navSvc.headerTitle.next("Tenant"); this.subscription = this.auth.userChanged.subscribe( @@ -182,7 +184,7 @@ export class TenantsComponent implements OnInit, OnDestroy { * @param a The action selected from the context menu. */ public handleContextMenu(a: ContextMenuActionEvent>): void { - console.log("action:", a); + this.log.debug("action:", a); } /** diff --git a/experimental/traffic-portal/src/app/core/users/user-details/user-details.component.ts b/experimental/traffic-portal/src/app/core/users/user-details/user-details.component.ts index 511143e626..21b14cff69 100644 --- a/experimental/traffic-portal/src/app/core/users/user-details/user-details.component.ts +++ b/experimental/traffic-portal/src/app/core/users/user-details/user-details.component.ts @@ -19,6 +19,7 @@ import type { PostRequestUser, ResponseRole, ResponseTenant, ResponseUser, User import { UserService } from "src/app/api"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; +import { LoggingService } from "src/app/shared/logging.service"; /** * UserDetailsComponent is the controller for the page for viewing/editing a @@ -39,9 +40,9 @@ export class UserDetailsComponent implements OnInit { constructor( private readonly userService: UserService, private readonly route: ActivatedRoute, - private readonly currentUserService: CurrentUserService - ) { - } + private readonly currentUserService: CurrentUserService, + private readonly log: LoggingService + ) { } /** Angular lifecycle hook */ public async ngOnInit(): Promise { @@ -51,7 +52,7 @@ export class UserDetailsComponent implements OnInit { ]); const ID = this.route.snapshot.paramMap.get("id"); if (ID === null) { - console.error("missing required route parameter 'id'"); + this.log.error("missing required route parameter 'id'"); return; } await rolesAndTenants; @@ -70,7 +71,7 @@ export class UserDetailsComponent implements OnInit { } const numID = parseInt(ID, 10); if (Number.isNaN(numID)) { - console.error("route parameter 'id' was non-number:", ID); + this.log.error("route parameter 'id' was non-number:", ID); return; } this.user = await this.userService.getUsers(numID); diff --git a/experimental/traffic-portal/src/app/core/users/user-registration-dialog/user-registration-dialog.component.ts b/experimental/traffic-portal/src/app/core/users/user-registration-dialog/user-registration-dialog.component.ts index b02294a6dd..a0ca6e1a24 100644 --- a/experimental/traffic-portal/src/app/core/users/user-registration-dialog/user-registration-dialog.component.ts +++ b/experimental/traffic-portal/src/app/core/users/user-registration-dialog/user-registration-dialog.component.ts @@ -17,6 +17,7 @@ import { ResponseRole, ResponseTenant } from "trafficops-types"; import { UserService } from "src/app/api"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; +import { LoggingService } from "src/app/shared/logging.service"; /** * Controller for a dialog that opens to register a new user. @@ -38,7 +39,8 @@ export class UserRegistrationDialogComponent implements OnInit { constructor( private readonly userService: UserService, private readonly auth: CurrentUserService, - private readonly dialogRef: MatDialogRef + private readonly dialogRef: MatDialogRef, + private readonly log: LoggingService, ) { } /** @@ -81,7 +83,7 @@ export class UserRegistrationDialogComponent implements OnInit { await this.userService.registerUser(this.email, this.role, this.tenant); this.dialogRef.close(); } catch (err) { - console.error("failed to register user:", err); + this.log.error("failed to register user:", err); } } } diff --git a/experimental/traffic-portal/src/app/login/login.component.spec.ts b/experimental/traffic-portal/src/app/login/login.component.spec.ts index 593c7d32a6..aac736a15a 100644 --- a/experimental/traffic-portal/src/app/login/login.component.spec.ts +++ b/experimental/traffic-portal/src/app/login/login.component.spec.ts @@ -83,11 +83,7 @@ describe("LoginComponent", () => { }); it("should exist", () => { - try{ - expect(component).toBeTruthy(); - } catch (e) { - console.error("error in 'should exist' for LoginComponent:", e); - } + expect(component).toBeTruthy(); }); it("submits a login request", async () => { diff --git a/experimental/traffic-portal/src/app/login/login.component.ts b/experimental/traffic-portal/src/app/login/login.component.ts index a70a5b0b5f..24083015d7 100644 --- a/experimental/traffic-portal/src/app/login/login.component.ts +++ b/experimental/traffic-portal/src/app/login/login.component.ts @@ -18,6 +18,7 @@ import { Router, ActivatedRoute, DefaultUrlSerializer } from "@angular/router"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; import { NavigationService } from "src/app/shared/navigation/navigation.service"; +import { LoggingService } from "../shared/logging.service"; import { AutocompleteValue } from "../utils"; import { ResetPasswordDialogComponent } from "./reset-password-dialog/reset-password-dialog.component"; @@ -50,7 +51,8 @@ export class LoginComponent implements OnInit { private readonly router: Router, private readonly auth: CurrentUserService, private readonly dialog: MatDialog, - private readonly navSvc: NavigationService + private readonly navSvc: NavigationService, + private readonly log: LoggingService, ) { this.navSvc.headerHidden.next(true); this.navSvc.sidebarHidden.next(true); @@ -74,7 +76,7 @@ export class LoginComponent implements OnInit { this.router.navigate(["/core/me"], {queryParams: {edit: true, updatePassword: true}}); } } catch (e) { - console.error("token login failed:", e); + this.log.error("token login failed:", e); } } } @@ -99,7 +101,7 @@ export class LoginComponent implements OnInit { this.router.navigate(tree.root.children.primary.segments.map(s=>s.path), {queryParams: tree.queryParams}); } } catch (err) { - console.error("login failed:", err); + this.log.error("login failed:", err); } } diff --git a/experimental/traffic-portal/src/app/shared/alert/alert.component.ts b/experimental/traffic-portal/src/app/shared/alert/alert.component.ts index f0b977967f..03f0caafdd 100644 --- a/experimental/traffic-portal/src/app/shared/alert/alert.component.ts +++ b/experimental/traffic-portal/src/app/shared/alert/alert.component.ts @@ -15,6 +15,8 @@ import { Component, OnDestroy } from "@angular/core"; import { MatSnackBar } from "@angular/material/snack-bar"; import { Subscription } from "rxjs"; +import { LoggingService } from "../logging.service"; + import { AlertService } from "./alert.service"; /** @@ -33,12 +35,10 @@ export class AlertComponent implements OnDestroy { /** The duration for which Alerts will linger until dismissed. `undefined` means forever. */ public duration: number | undefined = 10000; - /** - * Constructor. - */ constructor( private readonly alerts: AlertService, - private readonly snackBar: MatSnackBar + private readonly snackBar: MatSnackBar, + log: LoggingService ) { this.subscription = this.alerts.alerts.subscribe( a => { @@ -48,23 +48,23 @@ export class AlertComponent implements OnDestroy { } switch (a.level) { case "success": - console.log("alert: ", a.text); + log.debug("alert:", a.text); break; case "info": - console.log("alert: ", a.text); + log.info("alert:", a.text); break; case "warning": - console.warn("alert: ", a.text); + log.warn("alert:", a.text); break; case "error": - console.error("alert: ", a.text); + log.error("alert:", a.text); break; } this.snackBar.open(a.text, "dismiss", {duration: this.duration, verticalPosition: "top"}); } }, e => { - console.error("Error in alerts subscription:", e); + log.error("Error in alerts subscription:", e); } ); } diff --git a/experimental/traffic-portal/src/app/shared/charts/linechart.directive.spec.ts b/experimental/traffic-portal/src/app/shared/charts/linechart.directive.spec.ts index 5e88b18514..266a5572b7 100644 --- a/experimental/traffic-portal/src/app/shared/charts/linechart.directive.spec.ts +++ b/experimental/traffic-portal/src/app/shared/charts/linechart.directive.spec.ts @@ -16,6 +16,8 @@ import { BehaviorSubject } from "rxjs"; import type { DataSet } from "src/app/models"; +import { LoggingService } from "../logging.service"; + import { LinechartDirective } from "./linechart.directive"; describe("LinechartDirective", () => { @@ -24,7 +26,7 @@ describe("LinechartDirective", () => { beforeEach(()=>{ dataSets = new BehaviorSubject|null>(null); - directive = new LinechartDirective(new ElementRef(document.createElement("canvas"))); + directive = new LinechartDirective(new ElementRef(document.createElement("canvas")), new LoggingService()); directive.chartDataSets = dataSets; directive.ngAfterViewInit(); }); diff --git a/experimental/traffic-portal/src/app/shared/charts/linechart.directive.ts b/experimental/traffic-portal/src/app/shared/charts/linechart.directive.ts index 2871b1e5a3..3545d64046 100644 --- a/experimental/traffic-portal/src/app/shared/charts/linechart.directive.ts +++ b/experimental/traffic-portal/src/app/shared/charts/linechart.directive.ts @@ -18,6 +18,8 @@ import { from, type Observable, type Subscription } from "rxjs"; import type { DataSet } from "src/app/models/data"; +import { LoggingService } from "../logging.service"; + /** * LinechartDirective decorates canvases by creating a rendering context for * ChartJS charts. @@ -54,7 +56,7 @@ export class LinechartDirective implements AfterViewInit, OnDestroy { /** Chart.js configuration options. */ private opts: Chart.ChartConfiguration = {}; - constructor(private readonly element: ElementRef) { } + constructor(private readonly element: ElementRef, private readonly log: LoggingService) { } /** * Initializes the chart using the input data. @@ -177,7 +179,7 @@ export class LinechartDirective implements AfterViewInit, OnDestroy { * @param e The error that occurred. */ private dataError(e: Error): void { - console.error("data error occurred:", e); + this.log.error("data error occurred:", e); this.destroyChart(); if (this.ctx) { this.ctx.font = "30px serif"; diff --git a/experimental/traffic-portal/src/app/shared/current-user/current-user.service.ts b/experimental/traffic-portal/src/app/shared/current-user/current-user.service.ts index 1eb1198806..6f97264d0b 100644 --- a/experimental/traffic-portal/src/app/shared/current-user/current-user.service.ts +++ b/experimental/traffic-portal/src/app/shared/current-user/current-user.service.ts @@ -18,6 +18,8 @@ import { Capability, ResponseCurrentUser } from "trafficops-types"; import { UserService } from "src/app/api"; +import { LoggingService } from "../logging.service"; + /** * This service keeps track of the currently authenticated user. * @@ -52,7 +54,7 @@ export class CurrentUserService { return this.currentUser !== null; } - constructor(private readonly router: Router, private readonly api: UserService) { + constructor(private readonly router: Router, private readonly api: UserService, private readonly log: LoggingService) { this.updateCurrentUser(); } @@ -86,7 +88,8 @@ export class CurrentUserService { } ).catch( e => { - console.error("Failed to update current user:", e); + const msg = e instanceof Error ? e.message : String(e); + this.log.error(`Failed to update current user: ${msg}`); return false; } ).finally(() => this.updatingUserPromise = null ); diff --git a/experimental/traffic-portal/src/app/shared/current-user/current-user.testing-service.spec.ts b/experimental/traffic-portal/src/app/shared/current-user/current-user.testing-service.spec.ts index 4d2d631cf2..52ab6eb70e 100644 --- a/experimental/traffic-portal/src/app/shared/current-user/current-user.testing-service.spec.ts +++ b/experimental/traffic-portal/src/app/shared/current-user/current-user.testing-service.spec.ts @@ -13,7 +13,9 @@ */ import { EventEmitter, Injectable } from "@angular/core"; import { BehaviorSubject } from "rxjs"; -import { Capability, ResponseCurrentUser } from "trafficops-types"; +import type { Capability, ResponseCurrentUser } from "trafficops-types"; + +import { LoggingService } from "../logging.service"; /** * This is a mock for the {@link CurrentUserService} service for testing. @@ -58,6 +60,8 @@ export class CurrentUserTestingService { public permissions: BehaviorSubject> = new BehaviorSubject(new Set(["ALL"])); public readonly loggedIn = true; + constructor(private readonly log: LoggingService) {} + /** * Gets the current user if currentuser is not already set * @@ -132,7 +136,7 @@ export class CurrentUserTestingService { */ public logout(withRedirect?: boolean): void { if (withRedirect) { - console.warn("testing service does not navigate!"); + this.log.warn("testing service does not navigate!"); } } } diff --git a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts index ec47410d85..61b7c69c60 100644 --- a/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts +++ b/experimental/traffic-portal/src/app/shared/generic-table/generic-table.component.ts @@ -45,6 +45,7 @@ import type { BehaviorSubject, Subscription } from "rxjs"; import { fuzzyScore } from "src/app/utils"; +import { LoggingService } from "../logging.service"; import { BooleanFilterComponent } from "../table-components/boolean-filter/boolean-filter.component"; import { EmailCellRendererComponent } from "../table-components/email-cell-renderer/email-cell-renderer.component"; import { SSHCellRendererComponent } from "../table-components/ssh-cell-renderer/ssh-cell-renderer.component"; @@ -405,7 +406,7 @@ export class GenericTableComponent implements OnInit, OnDestroy { return (this.columnAPI.getColumns() ?? []).reverse(); } - constructor(private readonly router: Router, private readonly route: ActivatedRoute) { + constructor(private readonly router: Router, private readonly route: ActivatedRoute, private readonly log: LoggingService) { this.gridOptions = { defaultColDef: { filter: true, @@ -478,7 +479,7 @@ export class GenericTableComponent implements OnInit, OnDestroy { this.gridAPI.setFilterModel(JSON.parse(filterState)); } } catch (e) { - console.error(`Failed to retrieve stored column sort info from localStorage (key=${this.context}_table_filter:`, e); + this.log.error(`Failed to retrieve stored column sort info from localStorage (key=${this.context}_table_filter:`, e); } setUpQueryParamFilter(this.route.snapshot.queryParamMap, this.cols, this.gridAPI); this.gridAPI.onFilterChanged(); @@ -494,13 +495,13 @@ export class GenericTableComponent implements OnInit, OnDestroy { const colstates = localStorage.getItem(`${this.context}_table_columns`); if (colstates) { if (!this.columnAPI.applyColumnState(JSON.parse(colstates))) { - console.error("Failed to load stored column state: one or more columns not found"); + this.log.error("Failed to load stored column state: one or more columns not found"); } } else { this.gridAPI.sizeColumnsToFit(); } } catch (e) { - console.error(`Failure to retrieve required column info from localStorage (key=${this.context}_table_columns):`, e); + this.log.error(`Failure to retrieve required column info from localStorage (key=${this.context}_table_columns):`, e); } } @@ -681,7 +682,7 @@ export class GenericTableComponent implements OnInit, OnDestroy { if (this.columnAPI) { const column = this.columnAPI.getColumn(col); if (!column) { - console.error(`Failed to set visibility for column '${col}': no such column`); + this.log.error(`Failed to set visibility for column '${col}': no such column`); return; } const visible = column.isVisible(); @@ -725,12 +726,12 @@ export class GenericTableComponent implements OnInit, OnDestroy { */ public onCellContextMenu(params: CellContextMenuEvent): void { if (!params.event || !(params.event instanceof MouseEvent)) { - console.warn("cellContextMenu fired with no underlying event"); + this.log.warn("cellContextMenu fired with no underlying event"); return; } if (!this.contextmenu) { - console.warn("element reference to 'contextmenu' still null after view init"); + this.log.warn("element reference to 'contextmenu' still null after view init"); return; } diff --git a/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.ts b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.ts index 6a20435ac1..270b6663ee 100644 --- a/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.ts +++ b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.ts @@ -18,6 +18,7 @@ import { MAT_DIALOG_DATA } from "@angular/material/dialog"; import { AlertLevel } from "trafficops-types"; import { AlertService } from "../alert/alert.service"; +import { LoggingService } from "../logging.service"; /** * Contains the structure of the data that {@link ImportJsonTxtComponent} @@ -74,16 +75,20 @@ export class ImportJsonTxtComponent { } /** - * Creates an instance of import json edit txt component. + * Constructor. * - * @param dialogRef Dialog manager - * @param alertService Alert service manager - * @param datePipe Default angular date pipe for formating date + * @param data Data passed as input to the component. + * @param dialogRef Angular dialog service. + * @param alertService Alerts service. + * @param datePipe Default Angular pipe used for formatting dates. + * @param log Logging service. */ constructor( @Inject(MAT_DIALOG_DATA) public readonly data: ImportJsonTxtComponentModel, private readonly alertService: AlertService, - private readonly datePipe: DatePipe) { } + private readonly datePipe: DatePipe, + private readonly log: LoggingService, + ) { } /** * Hosts listener for drag over @@ -137,7 +142,7 @@ export class ImportJsonTxtComponent { */ public uploadFile(event: Event): void { if (!(event.target instanceof HTMLInputElement) || !event.target.files) { - console.warn("file uploading triggered on non-file-input element:", event.target); + this.log.warn("file uploading triggered on non-file-input element:", event.target); return; } diff --git a/experimental/traffic-portal/src/app/shared/interceptor/error.interceptor.ts b/experimental/traffic-portal/src/app/shared/interceptor/error.interceptor.ts index 55bc33993e..de36720f85 100644 --- a/experimental/traffic-portal/src/app/shared/interceptor/error.interceptor.ts +++ b/experimental/traffic-portal/src/app/shared/interceptor/error.interceptor.ts @@ -19,6 +19,7 @@ import { catchError } from "rxjs/operators"; import type { Alert } from "trafficops-types"; import { AlertService } from "../alert/alert.service"; +import { LoggingService } from "../logging.service"; /** * This class intercepts any and all HTTP error responses and checks for @@ -29,7 +30,8 @@ export class ErrorInterceptor implements HttpInterceptor { constructor( private readonly alerts: AlertService, - private readonly router: Router + private readonly router: Router, + private readonly log: LoggingService, ) {} /** @@ -54,7 +56,12 @@ export class ErrorInterceptor implements HttpInterceptor { */ public intercept(request: HttpRequest, next: HttpHandler): Observable> { return next.handle(request).pipe(catchError((err: HttpErrorResponse) => { - console.error("HTTP Error: ", err); + // I don't know why, but sometimes these errors have just no content + // and stringify to simply just the word "Error". So in order to get + // anything at all useful out of them, I'm adding a stack trace at + // the debugging level. + this.log.error(`HTTP error: ${err.message || err.error || err}`); + this.log.debug(err); if (typeof(err.error) === "string") { try { @@ -63,7 +70,7 @@ export class ErrorInterceptor implements HttpInterceptor { this.raiseAlerts(body.alerts); } } catch (e) { - console.error("non-JSON HTTP error response:", e); + this.log.error("non-JSON HTTP error response:", e); } } else if (typeof(err.error) === "object" && Array.isArray(err.error.alerts)) { this.raiseAlerts(err.error.alerts); diff --git a/experimental/traffic-portal/src/app/shared/loading/loading.component.spec.ts b/experimental/traffic-portal/src/app/shared/loading/loading.component.spec.ts index 6c1709ba62..a424ad75db 100644 --- a/experimental/traffic-portal/src/app/shared/loading/loading.component.spec.ts +++ b/experimental/traffic-portal/src/app/shared/loading/loading.component.spec.ts @@ -37,10 +37,6 @@ describe("LoadingComponent", () => { }); afterAll(() => { - try{ - TestBed.resetTestingModule(); - } catch (e) { - console.error("error in LoadingComponent afterAll:", e); - } + TestBed.resetTestingModule(); }); }); diff --git a/experimental/traffic-portal/src/app/shared/logging.service.spec.ts b/experimental/traffic-portal/src/app/shared/logging.service.spec.ts new file mode 100644 index 0000000000..ba844b49a8 --- /dev/null +++ b/experimental/traffic-portal/src/app/shared/logging.service.spec.ts @@ -0,0 +1,60 @@ +/** + * @license Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestBed } from "@angular/core/testing"; + +import { LoggingService } from "./logging.service"; + +describe("LoggingService", () => { + let service: LoggingService; + const arg = "test"; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LoggingService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("logs debug messages", () => { + const debugSpy = spyOn(console, "debug"); + expect(debugSpy).not.toHaveBeenCalled(); + service.debug(arg); + expect(debugSpy).toHaveBeenCalledTimes(1); + }); + + it("logs error messages", () => { + const errorSpy = spyOn(console, "error"); + expect(errorSpy).not.toHaveBeenCalled(); + service.error(arg); + expect(errorSpy).toHaveBeenCalledTimes(1); + }); + + it("logs info messages", () => { + const infoSpy = spyOn(console, "info"); + expect(infoSpy).not.toHaveBeenCalled(); + service.info(arg); + expect(infoSpy).toHaveBeenCalledTimes(1); + }); + + it("logs warning messages", () => { + const warnSpy = spyOn(console, "warn"); + expect(warnSpy).not.toHaveBeenCalled(); + service.warn(arg); + expect(warnSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/experimental/traffic-portal/src/app/shared/logging.service.ts b/experimental/traffic-portal/src/app/shared/logging.service.ts new file mode 100644 index 0000000000..73b6bb7512 --- /dev/null +++ b/experimental/traffic-portal/src/app/shared/logging.service.ts @@ -0,0 +1,74 @@ +/** + * @license Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from "@angular/core"; + +import { LogLevel, Logger } from "../utils"; + +/** + * LoggingService is for logging things in a consistent way across the UI. + * + * It's basically just a thin wrapper around a {@link Logger} so that only one + * instance needs to exist and injection makes its setup consistent across all + * usages. + */ +@Injectable({ + providedIn: "root" +}) +export class LoggingService { + + public logger: Logger; + + constructor() { + this.logger = new Logger(console, LogLevel.DEBUG, "", false); + } + + /** + * Logs a debugging message. + * + * @param args Anything you want to log. + */ + public debug(...args: unknown[]): void { + this.logger.debug(...args); + } + + /** + * Logs an error message. + * + * @param args Anything you want to log. + */ + public error(...args: unknown[]): void { + this.logger.error(...args); + } + + /** + * Logs an informational message. + * + * @param args Anything you want to log. + */ + public info(...args: unknown[]): void { + this.logger.info(...args); + } + + /** + * Logs a warning message. + * + * @param args Anything you want to log. + */ + public warn(...args: unknown[]): void { + this.logger.warn(...args); + } +} diff --git a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts index fc762b7069..faa1d87a92 100644 --- a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts +++ b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts @@ -19,6 +19,8 @@ import { UserService } from "src/app/api"; import { LOCAL_TPV1_URL } from "src/app/app.component"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; +import { LoggingService } from "../logging.service"; + /** * Defines the type of the header nav */ @@ -67,7 +69,9 @@ export class NavigationService { constructor( private readonly auth: CurrentUserService, private readonly api: UserService, - @Inject(PLATFORM_ID) private readonly platformId: object) { + @Inject(PLATFORM_ID) private readonly platformId: object, + private readonly log: LoggingService, + ) { if (isPlatformBrowser(this.platformId)) { this.tpv1Url = window.localStorage.getItem(LOCAL_TPV1_URL) ?? this.tpv1Url; } @@ -291,7 +295,7 @@ export class NavigationService { */ public async logout(): Promise { if (!(await this.api.logout())) { - console.warn("Failed to log out - clearing user data anyway!"); + this.log.warn("Failed to log out - clearing user data anyway!"); } this.auth.logout(); } diff --git a/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.spec.ts b/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.spec.ts index d7fee1e2f9..0ec720c429 100644 --- a/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.spec.ts +++ b/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.spec.ts @@ -54,10 +54,6 @@ describe("TpHeaderComponent", () => { }); afterAll(() => { - try{ - TestBed.resetTestingModule(); - } catch (e) { - console.error("error in TpHeaderComponent afterAll:", e); - } + TestBed.resetTestingModule(); }); }); diff --git a/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.ts b/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.ts index b9572489ea..b84ef8bf7b 100644 --- a/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.ts +++ b/experimental/traffic-portal/src/app/shared/navigation/tp-header/tp-header.component.ts @@ -13,8 +13,9 @@ */ import {Component, OnInit} from "@angular/core"; -import {HeaderNavigation, HeaderNavType, NavigationService} from "src/app/shared/navigation/navigation.service"; -import {ThemeManagerService} from "src/app/shared/theme-manager/theme-manager.service"; +import { LoggingService } from "src/app/shared/logging.service"; +import { HeaderNavigation, HeaderNavType, NavigationService } from "src/app/shared/navigation/navigation.service"; +import { ThemeManagerService } from "src/app/shared/theme-manager/theme-manager.service"; /** * TpHeaderComponent is the controller for the standard Traffic Portal header. @@ -58,8 +59,11 @@ export class TpHeaderComponent implements OnInit { }); } - constructor(public readonly themeSvc: ThemeManagerService, private readonly headerSvc: NavigationService) { - } + constructor( + public readonly themeSvc: ThemeManagerService, + private readonly headerSvc: NavigationService, + private readonly log: LoggingService, + ) { } /** * Calls a navs click function, throws an error if null @@ -82,7 +86,7 @@ export class TpHeaderComponent implements OnInit { */ public navRouterLink(nav: HeaderNavigation): string { if(nav.routerLink === undefined) { - console.error(`nav ${nav.text} does not have a routerLink`); + this.log.error(`nav ${nav.text} does not have a routerLink`); return ""; } return nav.routerLink; diff --git a/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.ts b/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.ts index 61fd89d8f0..28a2e8e7dc 100644 --- a/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.ts +++ b/experimental/traffic-portal/src/app/shared/navigation/tp-sidebar/tp-sidebar.component.ts @@ -20,6 +20,7 @@ import { Router, RouterEvent, Event, NavigationEnd, IsActiveMatchOptions } from import { filter } from "rxjs/operators"; import { CurrentUserService } from "src/app/shared/current-user/current-user.service"; +import { LoggingService } from "src/app/shared/logging.service"; import { NavigationService, TreeNavNode } from "src/app/shared/navigation/navigation.service"; /** @@ -92,10 +93,12 @@ export class TpSidebarComponent implements OnInit, AfterViewInit { return !this.childToParent.has(this.nodeHandle(node)); } - constructor(private readonly navService: NavigationService, + constructor( + private readonly navService: NavigationService, private readonly route: Router, - public readonly user: CurrentUserService) { - } + public readonly user: CurrentUserService, + private readonly log: LoggingService, + ) { } /** * Adds to childToParent from the given node. @@ -119,11 +122,11 @@ export class TpSidebarComponent implements OnInit, AfterViewInit { this.navService.sidebarHidden.subscribe(hidden => { if(hidden && this.sidenav.opened) { this.sidenav.close().catch(err => { - console.error(`Unable to close sidebar: ${err}`); + this.log.error(`Unable to close sidebar: ${err}`); }); } else if (!hidden && !this.sidenav.opened) { this.sidenav.open().catch(err => { - console.error(`Unable to open sidebar: ${err}`); + this.log.error(`Unable to open sidebar: ${err}`); }); } }); @@ -157,7 +160,7 @@ export class TpSidebarComponent implements OnInit, AfterViewInit { this.treeCtrl.expand(parent); parent = this.childToParent.get(this.nodeHandle(parent)); if(depth++ > 5) { - console.error(`Maximum depth ${depth} reached, aborting expand on ${parent?.name ?? "unknown"}`); + this.log.error(`Maximum depth ${depth} reached, aborting expand on ${parent?.name ?? "unknown"}`); break; } } diff --git a/experimental/traffic-portal/src/app/shared/shared.module.ts b/experimental/traffic-portal/src/app/shared/shared.module.ts index b52520be22..d271e98a21 100644 --- a/experimental/traffic-portal/src/app/shared/shared.module.ts +++ b/experimental/traffic-portal/src/app/shared/shared.module.ts @@ -32,6 +32,7 @@ import { AlertInterceptor } from "./interceptor/alerts.interceptor"; import { DateReviverInterceptor } from "./interceptor/date-reviver.interceptor"; import { ErrorInterceptor } from "./interceptor/error.interceptor"; import { LoadingComponent } from "./loading/loading.component"; +import { LoggingService } from "./logging.service"; import { ObscuredTextInputComponent } from "./obscured-text-input/obscured-text-input.component"; import { BooleanFilterComponent } from "./table-components/boolean-filter/boolean-filter.component"; import { EmailCellRendererComponent } from "./table-components/email-cell-renderer/email-cell-renderer.component"; @@ -88,7 +89,8 @@ import { CustomvalidityDirective } from "./validation/customvalidity.directive"; { multi: true, provide: HTTP_INTERCEPTORS, useClass: AlertInterceptor }, { multi: true, provide: HTTP_INTERCEPTORS, useClass: DateReviverInterceptor }, FileUtilsService, - DatePipe + DatePipe, + LoggingService ] }) export class SharedModule { } diff --git a/experimental/traffic-portal/src/app/shared/table-components/boolean-filter/boolean-filter.component.ts b/experimental/traffic-portal/src/app/shared/table-components/boolean-filter/boolean-filter.component.ts index cf4f95f305..959f5398f1 100644 --- a/experimental/traffic-portal/src/app/shared/table-components/boolean-filter/boolean-filter.component.ts +++ b/experimental/traffic-portal/src/app/shared/table-components/boolean-filter/boolean-filter.component.ts @@ -12,10 +12,11 @@ * limitations under the License. */ import { Component } from "@angular/core"; -// import { FormControl } from "@angular/forms"; import { AgFilterComponent } from "ag-grid-angular"; import { IDoesFilterPassParams, IFilterParams } from "ag-grid-community"; +import { LoggingService } from "src/app/shared/logging.service"; + /** A model that fully describes the state of a Boolean Filter. */ interface BooleanFilterModel { /** Whether or not filtering *should* be done. */ @@ -45,6 +46,8 @@ export class BooleanFilterComponent implements AgFilterComponent { /** Initialization parameters. */ private params!: IFilterParams; + constructor(private readonly log: LoggingService) {} + /** * Called by AG-Grid to check if the filter is in effect. * @@ -130,7 +133,7 @@ export class BooleanFilterComponent implements AgFilterComponent { public agInit(params: IFilterParams): void { this.params = params; if (!params.colDef.field) { - console.error("No column name found on boolean-filter parameters"); + this.log.error("No column name found on boolean-filter parameters"); return; } this.field = params.colDef.field; diff --git a/experimental/traffic-portal/src/app/shared/theme-manager/theme-manager.service.ts b/experimental/traffic-portal/src/app/shared/theme-manager/theme-manager.service.ts index ff7cb1e518..8338df1ad9 100644 --- a/experimental/traffic-portal/src/app/shared/theme-manager/theme-manager.service.ts +++ b/experimental/traffic-portal/src/app/shared/theme-manager/theme-manager.service.ts @@ -12,8 +12,10 @@ * limitations under the License. */ -import {DOCUMENT} from "@angular/common"; -import {EventEmitter, Inject, Injectable} from "@angular/core"; +import { DOCUMENT } from "@angular/common"; +import { EventEmitter, Inject, Injectable } from "@angular/core"; + +import { LoggingService } from "../logging.service"; /** * Defines a theme. If fileName is null, it is the default theme @@ -24,7 +26,8 @@ export interface Theme { } /** - * + * The ThemeManagerService manages the user's theming settings, to be applied + * throughout the UI. */ @Injectable({ providedIn: "root" @@ -35,7 +38,20 @@ export class ThemeManagerService { public themeChanged = new EventEmitter(); - constructor(@Inject(DOCUMENT) private readonly document: Document) { + /** + * Provides a "safe" accessor for the local session storage. According to + * typings, `Document.defaultView` may be `null`, but if it isn't then + * `Document.defaultView.localStorage` definitely *isn't* `null`. That's + * simply untrue. So this provides that check for you. + */ + private get localStorage(): Storage | null { + if (this.document.defaultView && this.document.defaultView.localStorage) { + return this.document.defaultView.localStorage; + } + return null; + } + + constructor(@Inject(DOCUMENT) private readonly document: Document, private readonly log: LoggingService) { this.initTheme(); } @@ -91,12 +107,10 @@ export class ThemeManagerService { * @param theme Theme to be stored */ private storeTheme(theme: Theme): void { - if(this.document.defaultView) { - try { - this.document.defaultView.localStorage.setItem(this.storageKey, JSON.stringify(theme)); - } catch (e) { - console.error(`Unable to store theme into local storage: ${e}`); - } + try { + this.localStorage?.setItem(this.storageKey, JSON.stringify(theme)); + } catch (e) { + this.log.error(`Unable to store theme into local storage: ${e}`); } } @@ -106,12 +120,10 @@ export class ThemeManagerService { * @returns The stored theme name or null */ private loadStoredTheme(): Theme | null { - if(this.document.defaultView) { - try { - return JSON.parse(this.document.defaultView.localStorage.getItem(this.storageKey) ?? "null"); - } catch (e) { - console.error(`Unable to load theme from local storage: ${e}`); - } + try { + return JSON.parse(this.localStorage?.getItem(this.storageKey) ?? "null"); + } catch (e) { + this.log.error(`Unable to load theme from local storage: ${e}`); } return null; } @@ -120,9 +132,7 @@ export class ThemeManagerService { * Clears theme saved in local storage */ private clearStoredTheme(): void { - if(this.document.defaultView) { - this.document.defaultView.localStorage.removeItem(this.storageKey); - } + this.localStorage?.removeItem(this.storageKey); } /** diff --git a/experimental/traffic-portal/src/app/utils/index.ts b/experimental/traffic-portal/src/app/utils/index.ts index 605c9818c3..af85a1f6f8 100644 --- a/experimental/traffic-portal/src/app/utils/index.ts +++ b/experimental/traffic-portal/src/app/utils/index.ts @@ -12,9 +12,10 @@ * limitations under the License. */ -export * from "./order-by"; export * from "./fuzzy"; export * from "./ip"; +export * from "./logging"; +export * from "./order-by"; export * from "./time"; /** diff --git a/experimental/traffic-portal/src/app/utils/logging.spec.ts b/experimental/traffic-portal/src/app/utils/logging.spec.ts new file mode 100644 index 0000000000..4a78081527 --- /dev/null +++ b/experimental/traffic-portal/src/app/utils/logging.spec.ts @@ -0,0 +1,373 @@ +/** + * @license Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Logger, LogLevel, logLevelToString, type LogStreams } from "./logging"; + +/** + * TestingStreams is a Streams implementation that pushes each log line to a + * publicly available array per stream, allowing for easy inspection by testing + * routines afterward. + */ +class TestingStreams implements LogStreams { + public readonly debugStream = new Array(); + public readonly errorStream = new Array(); + public readonly infoStream = new Array(); + public readonly warnStream = new Array(); + + /** + * Logs to the debug stream. + * + * @param args anything + */ + public debug(...args: unknown[]): void { + this.debugStream.push(args.join(" ")); + } + /** + * Logs to the debug stream. + * + * @param args anything + */ + public error(...args: unknown[]): void { + this.errorStream.push(args.join(" ")); + } + /** + * Logs to the info stream. + * + * @param args anything + */ + public info(...args: unknown[]): void { + this.infoStream.push(args.join(" ")); + } + /** + * Logs to the warning stream. + * + * @param args anything + */ + public warn(...args: unknown[]): void { + this.warnStream.push(args.join(" ")); + } +} + +const timestampPattern = "\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\.\\d+Z"; + +describe("logging utility functions", () => { + it("converts debug level to a string", () => { + expect(logLevelToString(LogLevel.DEBUG)).toBe("DEBUG"); + }); + it("converts error level to a string", () => { + expect(logLevelToString(LogLevel.ERROR)).toBe("ERROR"); + }); + it("converts info level to a string", () => { + expect(logLevelToString(LogLevel.INFO)).toBe("INFO"); + }); + it("converts warn level to a string", () => { + expect(logLevelToString(LogLevel.WARN)).toBe("WARN"); + }); +}); + +describe("Logger", () => { + let streams: TestingStreams; + + beforeEach(() => { + streams = new TestingStreams(); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + + describe("prefix-less logging", () => { + let logger: Logger; + const msg = "testquest"; + beforeEach(() => { + logger = new Logger(streams, LogLevel.DEBUG, "", false, false); + }); + + it("logs to the debug stream", () => { + logger.debug(msg); + expect(streams.debugStream).toHaveSize(1); + expect(streams.debugStream).toContain(msg); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the error stream", () => { + logger.error(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(1); + expect(streams.errorStream).toContain(msg); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the info stream", () => { + logger.info(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(1); + expect(streams.infoStream).toContain(msg); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the warn stream", () => { + logger.warn(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(1); + expect(streams.warnStream).toContain(msg); + }); + }); + + describe("static prefixed logging", () => { + let logger: Logger; + const prefix = "test"; + const msg = "quest"; + beforeEach(() => { + logger = new Logger(streams, LogLevel.DEBUG, prefix, false, false); + }); + + it("logs to the debug stream", () => { + logger.debug(msg); + expect(streams.debugStream).toHaveSize(1); + expect(streams.debugStream).toContain(`${prefix}: ${msg}`); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the error stream", () => { + logger.error(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(1); + expect(streams.errorStream).toContain(`${prefix}: ${msg}`); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the info stream", () => { + logger.info(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(1); + expect(streams.infoStream).toContain(`${prefix}: ${msg}`); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the warn stream", () => { + logger.warn(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(1); + expect(streams.warnStream).toContain(`${prefix}: ${msg}`); + }); + }); + + describe("timestamp-prefixed logging", () => { + let logger: Logger; + const msg = "testquest"; + beforeEach(() => { + logger = new Logger(streams, LogLevel.DEBUG, "", false, true); + }); + + it("logs to the debug stream", () => { + logger.debug(msg); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + if (streams.debugStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.debugStream.length}`); + } + expect(streams.debugStream[0]).toMatch(`^${timestampPattern}: ${msg}$`); + }); + + it("logs to the error stream", () => { + logger.error(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + if (streams.errorStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.errorStream.length}`); + } + expect(streams.errorStream[0]).toMatch(`^${timestampPattern}: ${msg}$`); + }); + + it("logs to the info stream", () => { + logger.info(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + if (streams.infoStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.infoStream.length}`); + } + expect(streams.infoStream[0]).toMatch(`^${timestampPattern}: ${msg}$`); + }); + + it("logs to the warn stream", () => { + logger.warn(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + if (streams.warnStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.warnStream.length}`); + } + expect(streams.warnStream[0]).toMatch(`^${timestampPattern}: ${msg}$`); + }); + }); + + describe("log-level-prefixed logging", () => { + let logger: Logger; + const msg = "testquest"; + beforeEach(() => { + logger = new Logger(streams, LogLevel.DEBUG, "", true, false); + }); + + it("logs to the debug stream", () => { + logger.debug(msg); + expect(streams.debugStream).toHaveSize(1); + expect(streams.debugStream).toContain(`${logLevelToString(LogLevel.DEBUG)}: ${msg}`); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the error stream", () => { + logger.error(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(1); + expect(streams.errorStream).toContain(`${logLevelToString(LogLevel.ERROR)}: ${msg}`); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the info stream", () => { + logger.info(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(1); + expect(streams.infoStream).toContain(`${logLevelToString(LogLevel.INFO)}: ${msg}`); + expect(streams.warnStream).toHaveSize(0); + }); + + it("logs to the warn stream", () => { + logger.warn(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(1); + expect(streams.warnStream).toContain(`${logLevelToString(LogLevel.WARN)}: ${msg}`); + }); + }); + + describe("fully-prefixed logging", () => { + let logger: Logger; + const prefix = "test"; + const msg = "quest"; + beforeEach(() => { + logger = new Logger(streams, LogLevel.DEBUG, prefix); + }); + + it("logs to the debug stream", () => { + logger.debug(msg); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + if (streams.debugStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.debugStream.length}`); + } + expect(streams.debugStream[0]).toMatch(`^${logLevelToString(LogLevel.DEBUG)} ${timestampPattern} ${prefix}: ${msg}$`); + }); + + it("logs to the error stream", () => { + logger.error(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + if (streams.errorStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.errorStream.length}`); + } + expect(streams.errorStream[0]).toMatch(`^${logLevelToString(LogLevel.ERROR)} ${timestampPattern} ${prefix}: ${msg}$`); + }); + + it("logs to the info stream", () => { + logger.info(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + if (streams.infoStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.infoStream.length}`); + } + expect(streams.infoStream[0]).toMatch(`^${logLevelToString(LogLevel.INFO)} ${timestampPattern} ${prefix}: ${msg}$`); + }); + + it("logs to the warn stream", () => { + logger.warn(msg); + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(0); + expect(streams.infoStream).toHaveSize(0); + if (streams.warnStream.length !== 1) { + return fail(`incorrect stream size after logging; want: 1, got: ${streams.warnStream.length}`); + } + expect(streams.warnStream[0]).toMatch(`^${logLevelToString(LogLevel.WARN)} ${timestampPattern} ${prefix}: ${msg}$`); + }); + }); + + describe("log-level specification", () => { + it("won't log above INFO if set to INFO", () => { + const logger = new Logger(streams, LogLevel.INFO); + + logger.debug("anything"); + logger.error("anything"); + logger.info("anything"); + logger.warn("anything"); + + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(1); + expect(streams.infoStream).toHaveSize(1); + expect(streams.warnStream).toHaveSize(1); + }); + + it("won't log above WARN if set to WARN", () => { + const logger = new Logger(streams, LogLevel.WARN); + + logger.debug("anything"); + logger.error("anything"); + logger.info("anything"); + logger.warn("anything"); + + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(1); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(1); + }); + + it("won't log above ERROR if set to ERROR", () => { + const logger = new Logger(streams, LogLevel.ERROR); + + logger.debug("anything"); + logger.error("anything"); + logger.info("anything"); + logger.warn("anything"); + + expect(streams.debugStream).toHaveSize(0); + expect(streams.errorStream).toHaveSize(1); + expect(streams.infoStream).toHaveSize(0); + expect(streams.warnStream).toHaveSize(0); + }); + }); +}); diff --git a/experimental/traffic-portal/src/app/utils/logging.ts b/experimental/traffic-portal/src/app/utils/logging.ts new file mode 100644 index 0000000000..7efafa8c3b --- /dev/null +++ b/experimental/traffic-portal/src/app/utils/logging.ts @@ -0,0 +1,227 @@ +/** + * @license Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * LogStreams are the underlying raw event writers used by {@link Logger}s. The + * simplest and most useful example of a LogStreams implementation is `console`. + */ +export interface LogStreams { + debug(...args: unknown[]): void; + info(...args: unknown[]): void; + error(...args: unknown[]): void; + warn(...args: unknown[]): void; +} + +/** + * A LogLevel describes the verbosity of logging. Each level is cumulative, + * meaning that a logger set to some level will also log all of the levels above + * it. + */ +export const enum LogLevel { + /** Log only errors. */ + ERROR, + /** Log warnings and errors. */ + WARN, + /** Log informational messages, warnings, and errors. */ + INFO, + /** Log debugging messages, informational messages, warnings, and errors. */ + DEBUG, +} + +/** + * Converts a log level to a human-readable string. + * + * @example + * console.log(logLevelToString(LogLevel.DEBUG)); + * // Output: + * // DEBUG + * + * @param level The level to convert. + * @returns A string representation of `level`. + */ +export function logLevelToString(level: LogLevel): string { + switch(level) { + case LogLevel.DEBUG: + return "DEBUG"; + case LogLevel.ERROR: + return "ERROR"; + case LogLevel.INFO: + return "INFO"; + case LogLevel.WARN: + return "WARN"; + } +} + +/** + * A Logger logs things. The output streams are customizable, mostly for testing + * but also in case we want to write directly to a file handle someday. + * + * The output format is a bit customizable, it allows for messages to be + * prefixed in a number of ways: + * - With the level at which the message was logged + * - With a timestamp for the time at which logging occurred (ISO format) + * - With some static string + * + * in that order. For example, if all of them are specified: + * + * @example + * (new Logger(console, LogLevel.DEBUG, "test", true, true)).info("quest"); + * // Output (example date is UNIX epoch): + * // INFO 1970-01-01T00:00:00.000Z test: quest + */ +export class Logger { + private readonly prefix: string; + + /** + * Constructor. + * + * @param streams The output stream abstractions. + * @param level The level at which the logger operates. Any level higher + * than the one specified will not be logged. + * @param prefix If given, prepends a prefix to each message. + * @param useLevelPrefixes If true, log lines will be prefixed with the name + * of the level at which they were logged (useful if all streams point to + * the same file descriptor). + * @param timestamps If true, each log line will be accompanied by a + * timestamp prefix (note that the time is determined when logging occurs, + * not necessarily when the logging method is called). + */ + constructor( + private readonly streams: LogStreams, + level: LogLevel, + prefix: string = "", + private readonly useLevelPrefixes: boolean = true, + private readonly timestamps: boolean = true, + ) { + if (prefix) { + prefix = prefix.trim().replace(/:$/, "").trimEnd(); + } + + this.prefix = prefix; + + const doNothing = (): void => { /* Do nothing */ }; + switch (level) { + case LogLevel.ERROR: + this.warn = doNothing; + case LogLevel.WARN: + this.info = doNothing; + case LogLevel.INFO: + this.debug = doNothing; + } + + // saves time later; getPrefix will make these same checks and return + // the same value if they all go the same way. + if (!this.timestamps && !this.useLevelPrefixes && !this.prefix) { + this.getPrefix = (): string => ""; + } + } + + /** + * Constructs a prefix for logging at a given level based on the Logger's + * configuration. + * + * @param level The level at which a message is being logged. + * @returns A prefix, or an empty string if no prefix is to be used. + */ + private getPrefix(level: LogLevel): string { + const parts = new Array(); + + if (this.timestamps) { + parts.push(new Date().toISOString()); + } + + if (this.useLevelPrefixes) { + parts.unshift(logLevelToString(level)); + } + + if (this.prefix) { + parts.push(this.prefix); + } + + // This colon isn't a problem, because if none of the above checks to + // add content to `parts` passed, the constructor would have optimized + // this whole method away. + return `${parts.join(" ")}:`; + } + + /** + * Logs a message at the DEBUG level. + * + * @param args Anything representable as text. Be careful passing objects; + * while technically allowed, this will probably cause multi-line log + * messages which are not easy to parse. Similarly, please don't use + * newlines. + */ + public debug(...args: unknown[]): void { + const prefix = this.getPrefix(LogLevel.DEBUG); + if (prefix) { + this.streams.debug(prefix, ...args); + return; + } + this.streams.debug(...args); + } + + /** + * Logs a message at the ERROR level. + * + * @param args Anything representable as text. Be careful passing objects; + * while technically allowed, this will probably cause multi-line log + * messages which are not easy to parse. Similarly, please don't use + * newlines. + */ + public error(...args: unknown[]): void { + const prefix = this.getPrefix(LogLevel.ERROR); + if (prefix) { + this.streams.error(prefix, ...args); + return; + } + this.streams.error(...args); + } + + /** + * Logs a message at the INFO level. + * + * @param args Anything representable as text. Be careful passing objects; + * while technically allowed, this will probably cause multi-line log + * messages which are not easy to parse. Similarly, please don't use + * newlines. + */ + public info(...args: unknown[]): void { + const prefix = this.getPrefix(LogLevel.INFO); + if (prefix) { + this.streams.info(prefix, ...args); + return; + } + this.streams.info(...args); + } + + /** + * Logs a message at the WARN level. + * + * @param args Anything representable as text. Be careful passing objects; + * while technically allowed, this will probably cause multi-line log + * messages which are not easy to parse. Similarly, please don't use + * newlines. + */ + public warn(...args: unknown[]): void { + const prefix = this.getPrefix(LogLevel.WARN); + if (prefix) { + this.streams.warn(prefix, ...args); + return; + } + this.streams.warn(...args); + } +} diff --git a/experimental/traffic-portal/src/app/utils/order-by.ts b/experimental/traffic-portal/src/app/utils/order-by.ts index e62997da02..f0100d045e 100644 --- a/experimental/traffic-portal/src/app/utils/order-by.ts +++ b/experimental/traffic-portal/src/app/utils/order-by.ts @@ -12,6 +12,10 @@ * limitations under the License. */ +import { environment } from "src/environments/environment"; + +import { LogLevel, Logger } from "./logging"; + /** * Implements a single comparison between two values * @@ -72,6 +76,7 @@ function cmpr(a: unknown, b: unknown): number { * @returns The sorted array */ export function orderBy(value: Array, property: string | Array): Array { + const logger = new Logger(console, environment.production ? LogLevel.INFO : LogLevel.DEBUG, "orderBy call", false); return value.sort((a: any, b: any) => { /* eslint-enable @typescript-eslint/no-explicit-any */ @@ -86,11 +91,11 @@ export function orderBy(value: Array, property: string | Array let bail = false; if (!Object.prototype.hasOwnProperty.call(a, p)) { - console.error("object", a, `has no property "${p}"!`); + logger.debug("object", a, `has no property "${p}"!`); bail = true; } if (!Object.prototype.hasOwnProperty.call(b, p)) { - console.error("object", b, `has no property "${p}"!`); + logger.debug("object", b, `has no property "${p}"!`); bail = true; } @@ -105,7 +110,7 @@ export function orderBy(value: Array, property: string | Array try { result = cmpr(aProp, bProp); } catch (e) { - console.error("property", p, "is not the same type on objects", a, "and", b, `! (${e})`); + logger.debug("property", p, "is not the same type on objects", a, "and", b, `! (${e})`); return 0; } diff --git a/experimental/traffic-portal/src/main.ts b/experimental/traffic-portal/src/main.ts index ca62c10ae2..fee4beeb8c 100644 --- a/experimental/traffic-portal/src/main.ts +++ b/experimental/traffic-portal/src/main.ts @@ -24,5 +24,11 @@ if (environment.production) { document.addEventListener("DOMContentLoaded", () => { platformBrowserDynamic().bootstrapModule(AppModule) + // Bootstrap failures will not be combined with logging service + // messages, because in that case no logging service could have been + // initialized. Therefore, consistency is unbroken, and for ease of + // debugging it's probably best not to mess with the format of Angular + // framework errors anyhow. + // eslint-disable-next-line no-console .catch(err => console.error(err)); });