You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by sh...@apache.org on 2023/08/11 13:45:45 UTC
[trafficcontrol] branch master updated: Tpv2 logging overhaul (#7673)
This is an automated email from the ASF dual-hosted git repository.
shamrick pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 08cd924ae6 Tpv2 logging overhaul (#7673)
08cd924ae6 is described below
commit 08cd924ae653273b2e800a46a6704f218f1e3fa0
Author: ocket8888 <oc...@apache.org>
AuthorDate: Fri Aug 11 07:45:39 2023 -0600
Tpv2 logging overhaul (#7673)
* Add a logging class
* re-name things to include a Log prefix
Because they're exported from utils, so the import wouldn't stutter in
most cases even if you decide to import the whole module as a namespace
(for whatever reason).
* Add middleware for logging and error handling
* Move file compression stuff into middleware
this simplifies it a bit and also switched to using fs/promises which is
a bit faster
* rework TO proxy handler to use a logger
* Add a front-end logging service
* fixup allowed console methods now that there's a logger
* Fix linting errors arising from not using loggers
* Remove console calls from tests
Also removes some try/catch blocks that appeared to be concealing test
failures?
* fix non-camelCase component naming
Also alphabetically sorted the declarations for the core module - we
were declaring a few things multiple times.
* Simplify type guards in server configuration
Makes use of the utils package to avoid the need for 'as' statements
* Add typing for a Node SystemError
* Fix incorrect use of logging middleware
* Try to clean up some errors being hit in the SSR handler
limited success on that front
* fix directory handle leak
* Make non-production server builds debug-able
* add initial debug log for all requests, fix double-writing error responses in some cases
* Move "time elapsed" debug message so that non-error responses also log it
* Remove duplicated check, extraneous substring argument
* Fix missing `req` option to SSR engine handler
* Update a lot of fs access to be asynchronous
* Fix hard-coded VERSION file path
* Cache file compressions instead of recalculating on every request
Made it like 14x faster on my machine - I was a real idiot to not do
that in the first place.
* Fix lint errors from rebasing
* Make regexp non-polynomial
* Fix comment grammar
---
experimental/traffic-portal/.eslintrc.json | 5 +-
experimental/traffic-portal/angular.json | 9 +-
experimental/traffic-portal/middleware.ts | 181 ++++++++++
.../traffic-portal/nightwatch/.eslintrc.json | 3 +-
experimental/traffic-portal/server.config.ts | 225 ++++++++-----
experimental/traffic-portal/server.ts | 295 ++++++++--------
.../src/app/api/delivery-service.service.ts | 15 +-
.../src/app/api/misc-apis.service.ts | 7 +-
.../traffic-portal/src/app/api/server.service.ts | 6 +-
.../src/app/api/testing/user.service.ts | 18 +-
.../asns/detail/asn-detail.component.spec.ts | 14 +-
.../asns/detail/asn-detail.component.ts | 21 +-
.../asns/table/asns-table.component.spec.ts | 10 +-
.../asns/table/asns-table.component.ts | 17 +-
.../cache-group-details.component.ts | 12 +-
.../cache-group-table.component.ts | 8 +-
.../detail/coordinate-detail.component.ts | 17 +-
.../divisions/detail/division-detail.component.ts | 17 +-
.../regions/detail/region-detail.component.ts | 18 +-
.../regions/table/regions-table.component.ts | 13 +-
.../core/cdns/cdn-detail/cdn-detail.component.ts | 10 +-
.../app/core/cdns/cdn-table/cdn-table.component.ts | 10 +-
.../certs/cert-detail/cert-detail.component.ts | 5 +-
.../certs/cert-viewer/cert-viewer.component.ts | 10 +-
.../traffic-portal/src/app/core/core.module.ts | 83 +++--
.../app/core/currentuser/currentuser.component.ts | 8 +-
.../update-password-dialog.component.ts | 8 +-
.../deliveryservice/deliveryservice.component.ts | 13 +-
.../deliveryservice/ds-card/ds-card.component.ts | 9 +-
.../invalidation-jobs.component.ts | 8 +-
.../new-invalidation-job-dialog.component.spec.ts | 8 +-
.../new-invalidation-job-dialog.component.ts | 8 +-
.../new-delivery-service.component.spec.ts | 38 +--
.../new-delivery-service.component.ts | 11 +-
.../detail/parameter-detail.component.ts | 17 +-
.../profile-detail/profile-detail.component.ts | 16 +-
.../servers/capabilities/capabilities.component.ts | 6 +-
.../phys-loc/detail/phys-loc-detail.component.ts | 20 +-
.../server-details/server-details.component.ts | 22 +-
.../update-status/update-status.component.ts | 13 +-
.../topology-details/topology-details.component.ts | 13 +-
.../app/core/types/detail/type-detail.component.ts | 17 +-
.../users/roles/detail/role-detail.component.ts | 16 +-
.../users/roles/table/roles-table.component.ts | 16 +-
.../tenant-details/tenant-details.component.ts | 19 +-
.../app/core/users/tenants/tenants.component.ts | 6 +-
.../users/user-details/user-details.component.ts | 11 +-
.../user-registration-dialog.component.ts | 6 +-
.../src/app/login/login.component.spec.ts | 6 +-
.../src/app/login/login.component.ts | 8 +-
.../src/app/shared/alert/alert.component.ts | 18 +-
.../app/shared/charts/linechart.directive.spec.ts | 4 +-
.../src/app/shared/charts/linechart.directive.ts | 6 +-
.../shared/current-user/current-user.service.ts | 7 +-
.../current-user.testing-service.spec.ts | 8 +-
.../generic-table/generic-table.component.ts | 15 +-
.../import-json-txt/import-json-txt.component.ts | 17 +-
.../app/shared/interceptor/error.interceptor.ts | 13 +-
.../app/shared/loading/loading.component.spec.ts | 6 +-
.../src/app/shared/logging.service.spec.ts | 60 ++++
.../src/app/shared/logging.service.ts | 74 ++++
.../app/shared/navigation/navigation.service.ts | 8 +-
.../tp-header/tp-header.component.spec.ts | 6 +-
.../navigation/tp-header/tp-header.component.ts | 14 +-
.../navigation/tp-sidebar/tp-sidebar.component.ts | 15 +-
.../traffic-portal/src/app/shared/shared.module.ts | 4 +-
.../boolean-filter/boolean-filter.component.ts | 7 +-
.../shared/theme-manager/theme-manager.service.ts | 48 +--
experimental/traffic-portal/src/app/utils/index.ts | 3 +-
.../traffic-portal/src/app/utils/logging.spec.ts | 373 +++++++++++++++++++++
.../traffic-portal/src/app/utils/logging.ts | 227 +++++++++++++
.../traffic-portal/src/app/utils/order-by.ts | 11 +-
experimental/traffic-portal/src/main.ts | 6 +
73 files changed, 1718 insertions(+), 584 deletions(-)
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>;
+}
+
+/**
+ * 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<string, StaticFile>;
+ 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<unknown, TPResponseLocals>;
+
+/**
+ * An HTTP request handler for the TP server.
+ */
+export type TPHandler = (req: Request, resp: TPResponseWriter, next: NextFunction) => void | PromiseLike<void>;
+
+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<string[]> {
+ const dir = await opendir(path);
+ let dirEnt = await dir.read();
+ let files = new Array<string>();
+
+ 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<TPHandler> {
+ 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<string, StaticFile>(
+ 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<void> => {
+ 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<string> {
+ 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<ServerVersion> {
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<ServerConfig> {
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<CompressionType>;
-}
+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<express.Express> {
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<string, StaticFile>(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<void>` 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<number> {
+ 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<TypeFromResponse>;
- /**
- * 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<TypeFromResponse>();
}
@@ -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<DSStats>(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<string, string>();
+ 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<HttpResponse<object> | 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<boolean> {
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<void> {
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<AsnDetailComponent>;
+ let component: ASNDetailComponent;
+ let fixture: ComponentFixture<ASNDetailComponent>;
let route: ActivatedRoute;
let paramMap: jasmine.Spy;
const headerSvc = jasmine.createSpyObj([],{headerHidden: new ReplaySubject<boolean>(), headerTitle: new ReplaySubject<string>()});
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<ResponseCacheGroup>;
- 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<void> {
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<AsnsTableComponent>;
+ let component: ASNsTableComponent;
+ let fixture: ComponentFixture<ASNsTableComponent>;
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<Array<ResponseASN>>;
- 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<string>("");
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<ResponseASN>): Promise<void> {
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<void> {
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<void> {
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<DecisionDialogComponent, DecisionDialogData, boolean>(
@@ -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<string>("");
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<void> {
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<void> {
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<void> {
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<void> {
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<ResponseDivision>;
- 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<void> {
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<Array<ResponseRegion>>;
- 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<string>("");
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<void> {
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<void> {
if (this.new) {
- console.error("Unable to delete new CDN");
+ this.log.error("Unable to delete new CDN");
return;
}
const ref = this.dialog.open<DecisionDialogComponent, DecisionDialogData, boolean>(
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<string>("");
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<UpdatePasswordDialogComponent>,
- 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<ResponseInvalidationJob>();
}
/**
* 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<void> {
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<NewInvalidationJobDialogComponent>;
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<NewInvalidationJobDialogComponent>;
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<NewInvalidationJobDialogComponent>,
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<void> {
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<void> {
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<void> {
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<string>("");
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<ResponseRegion>;
- 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<void> {
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<DecisionDialogComponent, DecisionDialogData, boolean>(
@@ -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<UpdateStatusComponent>,
+ constructor(
+ private readonly dialogRef: MatDialogRef<UpdateStatusComponent>,
@Inject(MAT_DIALOG_DATA) private readonly dialogServers: Array<ResponseServer>,
- 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<void> {
if (this.new) {
- console.error("Unable to delete new Topology");
+ this.log.error("Unable to delete new Topology");
return;
}
const ref = this.dialog.open<DecisionDialogComponent, DecisionDialogData, boolean>(
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<void> {
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<void> {
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<void> {
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<Array<ResponseRole>>;
- 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<string>("");
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<ResponseRole>): Promise<void> {
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<ResponseTenant>();
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<void> {
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<void> {
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<Readonly<ResponseTenant>>): 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<void> {
@@ -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<UserRegistrationDialogComponent>
+ private readonly dialogRef: MatDialogRef<UserRegistrationDialogComponent>,
+ 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<Array<DataSet | null>|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<Set<string>> = 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<T> 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<T> 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<T> 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<T> 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<T> 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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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<void> {
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<Theme>();
- 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<string>();
+ public readonly errorStream = new Array<string>();
+ public readonly infoStream = new Array<string>();
+ public readonly warnStream = new Array<string>();
+
+ /**
+ * 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<string>();
+
+ 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<T extends any>(value: Array<T>, property: string | Array<string>): Array<T> {
+ 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<T extends any>(value: Array<T>, 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<T extends any>(value: Array<T>, 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));
});