Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Early logs fix #58

Merged
merged 6 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
yarn.lock linguist-generated
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to process",
"type": "node",
"request": "attach",
"port": 9229,
"skipFiles": [
// Node.js internal core modules
"<node_internals>/**",

// Ignore all dependencies (optional)
// "${workspaceFolder}/node_modules/**",
// "!${workspaceFolder}/node_modules/@digital-alchemy/**",
],
}
]
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"lint": "eslint src",
"prepublishOnly": "yarn build",
"test": "./scripts/test.sh",
"upgrade": "ncu -u; yarn"
"upgrade": "yarn up '@digital-alchemy/*'"
},
"bugs": {
"email": "[email protected]",
Expand Down
31 changes: 26 additions & 5 deletions src/extensions/logger.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export async function Logger({ lifecycle, config, internal }: TServiceParams) {
.slice(SYMBOL_START, SYMBOL_END)
.join("\n");
}
if (["warn", "error", "log"].includes(key)) {
if (["warn", "error", "fatal"].includes(key)) {
// eslint-disable-next-line no-console
console.error(message);
return;
Expand Down Expand Up @@ -206,46 +206,67 @@ export async function Logger({ lifecycle, config, internal }: TServiceParams) {
}

const updateShouldLog = () => {
CURRENT_LOG_LEVEL = config.boilerplate.LOG_LEVEL;
if (!is.empty(config.boilerplate.LOG_LEVEL)) {
CURRENT_LOG_LEVEL = config.boilerplate.LOG_LEVEL;
}
LOG_LEVELS.forEach((key: TConfigLogLevel) => {
shouldILog[key] =
LOG_LEVEL_PRIORITY[key] >= LOG_LEVEL_PRIORITY[CURRENT_LOG_LEVEL];
});
};
// #endregion

// #MARK: lifecycle
lifecycle.onPostConfig(updateShouldLog);
lifecycle.onPostConfig(() => internal.boilerplate.logger.updateShouldLog());
internal.boilerplate.configuration.onUpdate(
updateShouldLog,
() => internal.boilerplate.logger.updateShouldLog(),
"boilerplate",
"LOG_LEVEL",
);
updateShouldLog();

// #MARK: return object
return {
/**
* Create a new logger instance for a given context
*/
context,

/**
* Retrieve a reference to the base logger used to emit from
*/
getBaseLogger: () => logger,

/**
* for testing
*/
getShouldILog: () => ({ ...shouldILog }),

/**
* exposed for testing
*/
prettyFormatMessage,

/**
* Modify the base logger
*
* Note: Extension still handles LOG_LEVEL logic
*/
setBaseLogger: (base: ILogger) => (logger = base),

/**
* Set the enabled/disabled state of the message pretty formatting logic
*/
setPrettyFormat: (state: boolean) => (prettyFormat = state),

/**
* Logger instance of last resort
*/
systemLogger: context("digital-alchemy:system-logger"),

/**
* exposed for testing
*/
updateShouldLog,
};
}
// #endregion
3 changes: 2 additions & 1 deletion src/extensions/wiring.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ async function WireService(
return loaded[service];
} catch (error) {
// Init errors at this level are considered blocking / fatal
(logger || console).error("initialization error", error);
// eslint-disable-next-line no-console
console.error("initialization error", error);
exit();
}
}
Expand Down
28 changes: 23 additions & 5 deletions src/helpers/utilities.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,41 @@
return out;
}

export const ACTIVE_THROTTLE = new Map<string, SleepReturn>();
export const ACTIVE_THROTTLE = new Set<string>();
export const ACTIVE_DEBOUNCE = new Map<string, SleepReturn>();

/**
* > πŸ¦ΆπŸ”« - careful about creating memory leaks!
* allow initial call, then block for a period
*/
export async function throttle(
identifier: string,
timeout: number,
): Promise<void> {
const current = ACTIVE_THROTTLE.get(identifier);
if (ACTIVE_THROTTLE.has(identifier)) {
return;

Check warning on line 104 in src/helpers/utilities.helper.ts

View check run for this annotation

Codecov / codecov/patch

src/helpers/utilities.helper.ts#L104

Added line #L104 was not covered by tests
}

ACTIVE_THROTTLE.add(identifier);

Check warning on line 107 in src/helpers/utilities.helper.ts

View check run for this annotation

Codecov / codecov/patch

src/helpers/utilities.helper.ts#L107

Added line #L107 was not covered by tests

await sleep(timeout);
ACTIVE_THROTTLE.delete(identifier);

Check warning on line 110 in src/helpers/utilities.helper.ts

View check run for this annotation

Codecov / codecov/patch

src/helpers/utilities.helper.ts#L109-L110

Added lines #L109 - L110 were not covered by tests
}

/**
* wait for duration after call before allowing next, extends for calls inside window
*/
export async function debounce(
identifier: string,
timeout: number,
): Promise<void> {
const current = ACTIVE_DEBOUNCE.get(identifier);
if (current) {
current.kill("stop");
}
const delay = sleep(timeout);
ACTIVE_THROTTLE.set(identifier, delay);
ACTIVE_DEBOUNCE.set(identifier, delay);
await delay;
ACTIVE_THROTTLE.delete(identifier);
ACTIVE_DEBOUNCE.delete(identifier);
}

export const asyncNoop = async () => await sleep(NONE);
Expand Down
168 changes: 168 additions & 0 deletions src/testing/logger.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/* eslint-disable unicorn/escape-case */
/* eslint-disable unicorn/no-hex-escape */
// magic import, do not remove / put anything above
import "..";

import { CreateApplication, is } from "../extensions";
import {
ApplicationDefinition,
BootstrapOptions,
OptionalModuleConfiguration,
ServiceMap,
TServiceParams,
} from "../helpers";

const BASIC_BOOT = {
configuration: {
boilerplate: { LOG_LEVEL: "silent" },
},
} as BootstrapOptions;

describe("Logger", () => {
let application: ApplicationDefinition<
ServiceMap,
OptionalModuleConfiguration
>;

afterEach(async () => {
if (application) {
await application.teardown();
application = undefined;
}
jest.restoreAllMocks();
});

describe("Configuration Interactions", () => {
it("can log stuff by default", async () => {
application = CreateApplication({
configurationLoaders: [],
// @ts-expect-error For unit testing
name: "testing",
services: {
Test({ internal }: TServiceParams) {
expect(is.empty(internal.boilerplate.logger.getShouldILog())).toBe(
false,
);
},
},
});
await application.bootstrap(BASIC_BOOT);
});

it("updates onPostConfig", async () => {
application = CreateApplication({
configurationLoaders: [],
// @ts-expect-error For unit testing
name: "testing",
services: {
Test({ internal, lifecycle }: TServiceParams) {
const spy = jest.spyOn(
internal.boilerplate.logger,
"updateShouldLog",
);
lifecycle.onReady(() => {
expect(spy).toHaveBeenCalled();
});
},
},
});
await application.bootstrap(BASIC_BOOT);
});

it("updates when LOG_LEVEL changes", async () => {
application = CreateApplication({
configurationLoaders: [],
// @ts-expect-error For unit testing
name: "testing",
services: {
Test({ internal }: TServiceParams) {
const spy = jest.spyOn(
internal.boilerplate.logger,
"updateShouldLog",
);
internal.boilerplate.configuration.set(
"boilerplate",
"LOG_LEVEL",
"warn",
);
expect(spy).toHaveBeenCalled();
},
},
});
await application.bootstrap(BASIC_BOOT);
});
});

describe("Pretty Formatting", () => {
let params: TServiceParams;
const getChalk = async () => (await import("chalk")).default;
let chalk: Awaited<ReturnType<typeof getChalk>>;
const frontDash = " - ";
let YELLOW_DASH: string;
let BLUE_TICK: string;

beforeAll(async () => {
chalk = await getChalk();
YELLOW_DASH = chalk.yellowBright(frontDash);
BLUE_TICK = chalk.blue(`>`);
application = CreateApplication({
configurationLoaders: [],
// @ts-expect-error For unit testing
name: "test",
services: {
Test(serviceParams: TServiceParams) {
params = serviceParams;
},
},
});
await application.bootstrap(BASIC_BOOT);
});

it("should return the original message if it exceeds MAX_CUTOFF", () => {
const longMessage = "a".repeat(2001);
expect(
params.internal.boilerplate.logger.prettyFormatMessage(longMessage),
).toBe(longMessage);
});

it("should highlight text with # in yellow", () => {
const message = "partA#partB";
const expected = chalk.yellow("partA#partB");
expect(
params.internal.boilerplate.logger.prettyFormatMessage(message),
).toBe(expected);
});

it('should highlight ">" in blue between square brackets', () => {
const message = "[A] > [B] > [C]";
const expected = `${chalk.bold.magenta("A")} ${BLUE_TICK} ${chalk.bold.magenta("B")} ${BLUE_TICK} ${chalk.bold.magenta("C")}`;
expect(
params.internal.boilerplate.logger.prettyFormatMessage(message),
).toBe(expected);
});

it("should strip brackets and highlight text in magenta", () => {
const message = "[Text]";
const expected = chalk.bold.magenta("Text");
expect(
params.internal.boilerplate.logger.prettyFormatMessage(message),
).toBe(expected);
});

it("should strip braces and highlight text in gray", () => {
const message = "{Text}";
const expected = chalk.bold.gray("Text");
expect(
params.internal.boilerplate.logger.prettyFormatMessage(message),
).toBe(expected);
});

it("should highlight dash at the start of the message in yellow", () => {
const message = " - Text";
const expected = `${YELLOW_DASH}Text`;
expect(
params.internal.boilerplate.logger.prettyFormatMessage(message),
).toBe(expected);
});
});
});
Loading
Loading