1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-10 13:09:37 +01:00

[PM-16792] add semantic logger facade (#13255)

This commit is contained in:
✨ Audrey ✨ 2025-02-07 12:15:05 -05:00 committed by GitHub
parent 00b19cf577
commit f9e2c20243
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 408 additions and 0 deletions

View File

@ -0,0 +1,199 @@
import { mock } from "jest-mock-extended";
import { LogService } from "../../platform/abstractions/log.service";
import { LogLevelType } from "../../platform/enums";
import { DefaultSemanticLogger } from "./default-semantic-logger";
const logger = mock<LogService>();
describe("DefaultSemanticLogger", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("debug", () => {
it("writes structural log messages to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
log.debug("this is a debug message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, {
message: "this is a debug message",
level: LogLevelType.Debug,
});
});
it("writes structural content to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
log.debug({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, {
content: { example: "this is content" },
level: LogLevelType.Debug,
});
});
it("writes structural content to console.log with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
log.info({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
content: { example: "this is content" },
message: "this is a message",
level: LogLevelType.Info,
});
});
});
describe("info", () => {
it("writes structural log messages to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
log.info("this is an info message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
message: "this is an info message",
level: LogLevelType.Info,
});
});
it("writes structural content to console.log", () => {
const log = new DefaultSemanticLogger(logger, {});
log.info({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
content: { example: "this is content" },
level: LogLevelType.Info,
});
});
it("writes structural content to console.log with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
log.info({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
content: { example: "this is content" },
message: "this is a message",
level: LogLevelType.Info,
});
});
});
describe("warn", () => {
it("writes structural log messages to console.warn", () => {
const log = new DefaultSemanticLogger(logger, {});
log.warn("this is a warning message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
message: "this is a warning message",
level: LogLevelType.Warning,
});
});
it("writes structural content to console.warn", () => {
const log = new DefaultSemanticLogger(logger, {});
log.warn({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
content: { example: "this is content" },
level: LogLevelType.Warning,
});
});
it("writes structural content to console.warn with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
log.warn({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
content: { example: "this is content" },
message: "this is a message",
level: LogLevelType.Warning,
});
});
});
describe("error", () => {
it("writes structural log messages to console.error", () => {
const log = new DefaultSemanticLogger(logger, {});
log.error("this is an error message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
message: "this is an error message",
level: LogLevelType.Error,
});
});
it("writes structural content to console.error", () => {
const log = new DefaultSemanticLogger(logger, {});
log.error({ example: "this is content" });
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
content: { example: "this is content" },
level: LogLevelType.Error,
});
});
it("writes structural content to console.error with a message", () => {
const log = new DefaultSemanticLogger(logger, {});
log.error({ example: "this is content" }, "this is a message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
content: { example: "this is content" },
message: "this is a message",
level: LogLevelType.Error,
});
});
});
describe("panic", () => {
it("writes structural log messages to console.error before throwing the message", () => {
const log = new DefaultSemanticLogger(logger, {});
expect(() => log.panic("this is an error message")).toThrow("this is an error message");
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
message: "this is an error message",
level: LogLevelType.Error,
});
});
it("writes structural log messages to console.error with a message before throwing the message", () => {
const log = new DefaultSemanticLogger(logger, {});
expect(() => log.panic({ example: "this is content" }, "this is an error message")).toThrow(
"this is an error message",
);
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
content: { example: "this is content" },
message: "this is an error message",
level: LogLevelType.Error,
});
});
it("writes structural log messages to console.error with a content before throwing the message", () => {
const log = new DefaultSemanticLogger(logger, {});
expect(() => log.panic("this is content", "this is an error message")).toThrow(
"this is an error message",
);
expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
content: "this is content",
message: "this is an error message",
level: LogLevelType.Error,
});
});
});
});

View File

@ -0,0 +1,65 @@
import { Jsonify } from "type-fest";
import { LogService } from "../../platform/abstractions/log.service";
import { LogLevelType } from "../../platform/enums";
import { SemanticLogger } from "./semantic-logger.abstraction";
/** Sends semantic logs to the console.
* @remarks the behavior of this logger is based on `LogService`; it
* replaces dynamic messages (`%s`) with a JSON-formatted semantic log.
*/
export class DefaultSemanticLogger<Context extends object> implements SemanticLogger {
/** Instantiates a console semantic logger
* @param context a static payload that is cloned when the logger
* logs a message. The `messages`, `level`, and `content` fields
* are reserved for use by loggers.
*/
constructor(
private logger: LogService,
context: Jsonify<Context>,
) {
this.context = context && typeof context === "object" ? context : {};
}
readonly context: object;
debug<T>(content: Jsonify<T>, message?: string): void {
this.log(content, LogLevelType.Debug, message);
}
info<T>(content: Jsonify<T>, message?: string): void {
this.log(content, LogLevelType.Info, message);
}
warn<T>(content: Jsonify<T>, message?: string): void {
this.log(content, LogLevelType.Warning, message);
}
error<T>(content: Jsonify<T>, message?: string): void {
this.log(content, LogLevelType.Error, message);
}
panic<T>(content: Jsonify<T>, message?: string): never {
this.log(content, LogLevelType.Error, message);
const panicMessage =
message ?? (typeof content === "string" ? content : "a fatal error occurred");
throw new Error(panicMessage);
}
private log<T>(content: Jsonify<T>, level: LogLevelType, message?: string) {
const log = {
...this.context,
message,
content: content ?? undefined,
level,
};
if (typeof content === "string" && !message) {
log.message = content;
delete log.content;
}
this.logger.write(level, log);
}
}

View File

@ -0,0 +1,18 @@
import { Jsonify } from "type-fest";
import { SemanticLogger } from "./semantic-logger.abstraction";
/** Disables semantic logs. Still panics. */
export class DisabledSemanticLogger implements SemanticLogger {
debug<T>(_content: Jsonify<T>, _message?: string): void {}
info<T>(_content: Jsonify<T>, _message?: string): void {}
warn<T>(_content: Jsonify<T>, _message?: string): void {}
error<T>(_content: Jsonify<T>, _message?: string): void {}
panic<T>(_content: Jsonify<T>, message?: string): never {
throw new Error(message);
}
}

View File

@ -0,0 +1,30 @@
import { Jsonify } from "type-fest";
import { LogService } from "../../platform/abstractions/log.service";
import { DefaultSemanticLogger } from "./default-semantic-logger";
import { DisabledSemanticLogger } from "./disabled-semantic-logger";
import { SemanticLogger } from "./semantic-logger.abstraction";
/** Instantiates a semantic logger that emits nothing when a message
* is logged.
* @param _context a static payload that is cloned when the logger
* logs a message. The `messages`, `level`, and `content` fields
* are reserved for use by loggers.
*/
export function disabledSemanticLoggerProvider<Context extends object>(
_context: Jsonify<Context>,
): SemanticLogger {
return new DisabledSemanticLogger();
}
/** Instantiates a semantic logger that emits logs to the console.
* @param context a static payload that is cloned when the logger
* logs a message. The `messages`, `level`, and `content` fields
* are reserved for use by loggers.
* @param settings specializes how the semantic logger functions.
* If this is omitted, the logger suppresses debug messages.
*/
export function consoleSemanticLoggerProvider(logger: LogService): SemanticLogger {
return new DefaultSemanticLogger(logger, {});
}

View File

@ -0,0 +1,2 @@
export { disabledSemanticLoggerProvider, consoleSemanticLoggerProvider } from "./factory";
export { SemanticLogger } from "./semantic-logger.abstraction";

View File

@ -0,0 +1,94 @@
import { Jsonify } from "type-fest";
/** Semantic/structural logging component */
export interface SemanticLogger {
/** Logs a message at debug priority.
* Debug messages are used for diagnostics, and are typically disabled
* in production builds.
* @param message - a message to record in the log's `message` field.
*/
debug(message: string): void;
/** Logs the content at debug priority.
* Debug messages are used for diagnostics, and are typically disabled
* in production builds.
* @param content - JSON content included in the log's `content` field.
* @param message - a message to record in the log's `message` field.
*/
debug<T>(content: Jsonify<T>, message?: string): void;
/** combined signature for overloaded methods */
debug<T>(content: Jsonify<T> | string, message?: string): void;
/** Logs a message at informational priority.
* Information messages are used for status reports.
* @param message - a message to record in the log's `message` field.
*/
info(message: string): void;
/** Logs the content at informational priority.
* Information messages are used for status reports.
* @param content - JSON content included in the log's `content` field.
* @param message - a message to record in the log's `message` field.
*/
info<T>(content: Jsonify<T>, message?: string): void;
/** combined signature for overloaded methods */
info<T>(content: Jsonify<T> | string, message?: string): void;
/** Logs a message at warn priority.
* Warn messages are used to indicate a operation that may affect system
* stability occurred.
* @param message - a message to record in the log's `message` field.
*/
warn(message: string): void;
/** Logs the content at warn priority.
* Warn messages are used to indicate a operation that may affect system
* stability occurred.
* @param content - JSON content included in the log's `content` field.
* @param message - a message to record in the log's `message` field.
*/
warn<T>(content: Jsonify<T>, message?: string): void;
/** combined signature for overloaded methods */
warn<T>(content: Jsonify<T> | string, message?: string): void;
/** Logs a message at error priority.
* Error messages are used to indicate a operation that affects system
* stability occurred and the system was able to recover.
* @param message - a message to record in the log's `message` field.
*/
error(message: string): void;
/** Logs the content at debug priority.
* Error messages are used to indicate a operation that affects system
* stability occurred and the system was able to recover.
* @param content - JSON content included in the log's `content` field.
* @param message - a message to record in the log's `message` field.
*/
error<T>(content: Jsonify<T>, message?: string): void;
/** combined signature for overloaded methods */
error<T>(content: Jsonify<T> | string, message?: string): void;
/** Logs a message at panic priority and throws an error.
* Panic messages are used to indicate a operation that affects system
* stability occurred and the system cannot recover. Panic messages
* log an error and throw an `Error`.
* @param message - a message to record in the log's `message` field.
*/
panic(message: string): never;
/** Logs the content at debug priority and throws an error.
* Panic messages are used to indicate a operation that affects system
* stability occurred and the system cannot recover. Panic messages
* log an error and throw an `Error`.
* @param content - JSON content included in the log's `content` field.
* @param message - a message to record in the log's `message` field.
*/
panic<T>(content: Jsonify<T>, message?: string): never;
/** combined signature for overloaded methods */
panic<T>(content: Jsonify<T> | string, message?: string): never;
}