Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f18e0855e | ||
|
|
44f471653e | ||
|
|
0ccb7f911d | ||
|
|
e0b450157d | ||
|
|
cebe107034 |
@@ -34,6 +34,7 @@ steps:
|
|||||||
- git remote set-url origin "https://${GITEA_REGISTRY_USERNAME}:${GITEA_RELEASE_TOKEN}@git.mifi.dev/mifi/logger.git"
|
- git remote set-url origin "https://${GITEA_REGISTRY_USERNAME}:${GITEA_RELEASE_TOKEN}@git.mifi.dev/mifi/logger.git"
|
||||||
- npm config set @mifi:registry https://git.mifi.dev/api/packages/mifi/npm/
|
- npm config set @mifi:registry https://git.mifi.dev/api/packages/mifi/npm/
|
||||||
- npm config set -- //git.mifi.dev/api/packages/mifi/npm/:_authToken "$NPM_TOKEN"
|
- npm config set -- //git.mifi.dev/api/packages/mifi/npm/:_authToken "$NPM_TOKEN"
|
||||||
|
- pnpm build
|
||||||
- pnpm release
|
- pnpm release
|
||||||
|
|
||||||
notify-release-failure:
|
notify-release-failure:
|
||||||
|
|||||||
+17
-4
@@ -1,8 +1,21 @@
|
|||||||
## [0.9.2](https://git.mifi.dev/mifi/logger/compare/v0.9.1...v0.9.2) (2026-08-05)
|
# [0.10.0](https://git.mifi.dev/mifi/logger/compare/v0.9.3...v0.10.0) (2026-08-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **logger:** add explicit Sentry event channel ([44f4716](https://git.mifi.dev/mifi/logger/commit/44f471653e8d4045e87886da1e1bbe04edbd60af))
|
||||||
|
|
||||||
|
## [0.9.3](https://git.mifi.dev/mifi/logger/compare/v0.9.2...v0.9.3) (2026-08-05)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **ci:** authenticate semantic release pushes ([1c63e58](https://git.mifi.dev/mifi/logger/commit/1c63e58809049e8a3a4da00601b09e60a5482ff1))
|
- **release:** build package before publishing ([e0b4501](https://git.mifi.dev/mifi/logger/commit/e0b450157df7a8aa1ff8bf5299140b0e7b52a7f7))
|
||||||
* **ci:** install CA certificates for release workflows ([d211d9f](https://git.mifi.dev/mifi/logger/commit/d211d9f8bc205ae6b952e35603773e44bca5ee37))
|
- **release:** build package before publishing ([cebe107](https://git.mifi.dev/mifi/logger/commit/cebe107034a15e9fd1517d872037e97337426b29))
|
||||||
* **ci:** install git for release workflows ([afd2ca1](https://git.mifi.dev/mifi/logger/commit/afd2ca1978d7373daa4b6ffa9067122586ba5f86))
|
|
||||||
|
## [0.9.2](https://git.mifi.dev/mifi/logger/compare/v0.9.1...v0.9.2) (2026-08-05)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **ci:** authenticate semantic release pushes ([1c63e58](https://git.mifi.dev/mifi/logger/commit/1c63e58809049e8a3a4da00601b09e60a5482ff1))
|
||||||
|
- **ci:** install CA certificates for release workflows ([d211d9f](https://git.mifi.dev/mifi/logger/commit/d211d9f8bc205ae6b952e35603773e44bca5ee37))
|
||||||
|
- **ci:** install git for release workflows ([afd2ca1](https://git.mifi.dev/mifi/logger/commit/afd2ca1978d7373daa4b6ffa9067122586ba5f86))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Containers can use `MIFI_LOG_LEVEL` and `MIFI_LOG_NAMESPACES`. Explicit `level`
|
|||||||
|
|
||||||
## Sentry
|
## Sentry
|
||||||
|
|
||||||
The Sentry adapter has no SDK dependency. Provide the SDK that your application already uses. In a production browser, errors go to Sentry without also appearing in the console. In Node, they go to both Sentry and stderr.
|
The Sentry adapter has no SDK dependency. Provide the SDK that your application already uses. In a production browser, errors go to Sentry without also appearing in the console. In Node, they go to both Sentry and stderr. Use `logger.sentry` to explicitly capture selected non-error events without changing the default policy or writing them to the console when they are otherwise suppressed.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
@@ -30,6 +30,8 @@ import { createLogger } from "@mifi/logger";
|
|||||||
import { createSentrySink } from "@mifi/logger/sentry";
|
import { createSentrySink } from "@mifi/logger/sentry";
|
||||||
|
|
||||||
const logger = createLogger({ environment: "production", sentry: createSentrySink(Sentry) });
|
const logger = createLogger({ environment: "production", sentry: createSentrySink(Sentry) });
|
||||||
|
|
||||||
|
logger.sentry.info("Cache warmed", { entries: 42 });
|
||||||
```
|
```
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mifi/logger",
|
"name": "@mifi/logger",
|
||||||
"version": "0.9.2",
|
"version": "0.10.0",
|
||||||
"description": "Intentional, namespaced logging for browser and Node.js TypeScript applications.",
|
"description": "Intentional, namespaced logging for browser and Node.js TypeScript applications.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
|
"prepack": "npm run build",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
|
|||||||
+18
-2
@@ -97,13 +97,20 @@ export function createLogger(options: LoggerOptions): Logger {
|
|||||||
levelRank(level) >= levelRank(threshold) &&
|
levelRank(level) >= levelRank(threshold) &&
|
||||||
(!runtimeOverride?.namespaces.length ||
|
(!runtimeOverride?.namespaces.length ||
|
||||||
namespaceMatches(namespace, runtimeOverride.namespaces));
|
namespaceMatches(namespace, runtimeOverride.namespaces));
|
||||||
const emit = (level: Exclude<LogLevel, "silent">, arguments_: readonly unknown[]) => {
|
const emit = (
|
||||||
if (!enabled(level)) return;
|
level: Exclude<LogLevel, "silent">,
|
||||||
|
arguments_: readonly unknown[],
|
||||||
|
sendToSentry = false,
|
||||||
|
) => {
|
||||||
|
const sendToConsole = enabled(level);
|
||||||
|
if (!sendToConsole && !sendToSentry) return;
|
||||||
const event: LogEvent = {
|
const event: LogEvent = {
|
||||||
level,
|
level,
|
||||||
namespace,
|
namespace,
|
||||||
arguments: evaluate(arguments_),
|
arguments: evaluate(arguments_),
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
|
sendToSentry,
|
||||||
|
sendToConsole,
|
||||||
};
|
};
|
||||||
sinks.forEach((sink) => sink.emit(event));
|
sinks.forEach((sink) => sink.emit(event));
|
||||||
};
|
};
|
||||||
@@ -123,6 +130,14 @@ export function createLogger(options: LoggerOptions): Logger {
|
|||||||
};
|
};
|
||||||
const api: Logger = {
|
const api: Logger = {
|
||||||
namespace,
|
namespace,
|
||||||
|
sentry: {
|
||||||
|
trace: (...items) => emit("trace", items, true),
|
||||||
|
debug: (...items) => emit("debug", items, true),
|
||||||
|
log: (...items) => emit("info", items, true),
|
||||||
|
info: (...items) => emit("info", items, true),
|
||||||
|
warn: (...items) => emit("warn", items, true),
|
||||||
|
error: (...items) => emit("error", items, true),
|
||||||
|
},
|
||||||
child: (child) =>
|
child: (child) =>
|
||||||
createLogger({ ...options, namespace: [namespace, child].filter(Boolean) as string[] }),
|
createLogger({ ...options, namespace: [namespace, child].filter(Boolean) as string[] }),
|
||||||
trace: (...items) => emit("trace", items),
|
trace: (...items) => emit("trace", items),
|
||||||
@@ -187,6 +202,7 @@ function supportsAnsi(): boolean {
|
|||||||
export function createConsoleSink(): LogSink {
|
export function createConsoleSink(): LogSink {
|
||||||
return {
|
return {
|
||||||
emit: (event) => {
|
emit: (event) => {
|
||||||
|
if (!event.sendToConsole) return;
|
||||||
const method = event.level === "trace" ? "debug" : event.level;
|
const method = event.level === "trace" ? "debug" : event.level;
|
||||||
const console_ = globalThis.console as Console & Record<string, unknown>;
|
const console_ = globalThis.console as Console & Record<string, unknown>;
|
||||||
const fn = console_[method];
|
const fn = console_[method];
|
||||||
|
|||||||
+3
-3
@@ -15,7 +15,7 @@ export interface SentryLike {
|
|||||||
export function createSentrySink(sentry: SentryLike): LogSink {
|
export function createSentrySink(sentry: SentryLike): LogSink {
|
||||||
return {
|
return {
|
||||||
emit(event: LogEvent) {
|
emit(event: LogEvent) {
|
||||||
if (event.level !== "error") return;
|
if (event.level !== "error" && !event.sendToSentry) return;
|
||||||
const error = event.arguments.find((item): item is Error => item instanceof Error);
|
const error = event.arguments.find((item): item is Error => item instanceof Error);
|
||||||
const extras = Object.fromEntries(
|
const extras = Object.fromEntries(
|
||||||
event.arguments
|
event.arguments
|
||||||
@@ -26,11 +26,11 @@ export function createSentrySink(sentry: SentryLike): LogSink {
|
|||||||
event.arguments.filter((item) => typeof item === "string").join(" ") ||
|
event.arguments.filter((item) => typeof item === "string").join(" ") ||
|
||||||
"Logger error";
|
"Logger error";
|
||||||
sentry.withScope((scope) => {
|
sentry.withScope((scope) => {
|
||||||
scope.setLevel("error");
|
scope.setLevel(event.level);
|
||||||
if (event.namespace) scope.setTag("logger.namespace", event.namespace);
|
if (event.namespace) scope.setTag("logger.namespace", event.namespace);
|
||||||
scope.setExtras(extras);
|
scope.setExtras(extras);
|
||||||
if (error) sentry.captureException(error);
|
if (error) sentry.captureException(error);
|
||||||
else sentry.captureMessage(message, "error");
|
else sentry.captureMessage(message, event.level);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export interface LogEvent {
|
|||||||
namespace?: string;
|
namespace?: string;
|
||||||
arguments: readonly unknown[];
|
arguments: readonly unknown[];
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
|
/** Whether this event was explicitly selected for Sentry capture. */
|
||||||
|
sendToSentry: boolean;
|
||||||
|
/** Whether the default console sink should render this event. */
|
||||||
|
sendToConsole: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A destination for enabled log events. */
|
/** A destination for enabled log events. */
|
||||||
@@ -33,8 +37,20 @@ export interface LoggerOptions {
|
|||||||
sinks?: readonly LogSink[];
|
sinks?: readonly LogSink[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Explicit Sentry reporting methods. These bypass the logger's normal level threshold. */
|
||||||
|
export interface SentryLogger {
|
||||||
|
trace(...arguments_: readonly unknown[]): void;
|
||||||
|
debug(...arguments_: readonly unknown[]): void;
|
||||||
|
log(...arguments_: readonly unknown[]): void;
|
||||||
|
info(...arguments_: readonly unknown[]): void;
|
||||||
|
warn(...arguments_: readonly unknown[]): void;
|
||||||
|
error(...arguments_: readonly unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Logger {
|
export interface Logger {
|
||||||
readonly namespace?: string;
|
readonly namespace?: string;
|
||||||
|
/** Sends selected events to a configured Sentry sink without changing the default policy for other logs. */
|
||||||
|
readonly sentry: SentryLogger;
|
||||||
child(namespace: string): Logger;
|
child(namespace: string): Logger;
|
||||||
trace(...arguments_: readonly unknown[]): void;
|
trace(...arguments_: readonly unknown[]): void;
|
||||||
debug(...arguments_: readonly unknown[]): void;
|
debug(...arguments_: readonly unknown[]): void;
|
||||||
|
|||||||
@@ -78,6 +78,26 @@ describe("createLogger", () => {
|
|||||||
consoleError.mockRestore();
|
consoleError.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sends explicitly selected production events to Sentry without writing them to the console", () => {
|
||||||
|
const destination = sink();
|
||||||
|
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
||||||
|
const logger = createLogger({
|
||||||
|
environment: "production",
|
||||||
|
runtime: "browser",
|
||||||
|
sentry: destination.sink,
|
||||||
|
});
|
||||||
|
logger.sentry.info("Cache warmed");
|
||||||
|
expect(destination.events).toHaveLength(1);
|
||||||
|
expect(destination.events[0]).toMatchObject({
|
||||||
|
level: "info",
|
||||||
|
arguments: ["Cache warmed"],
|
||||||
|
sendToSentry: true,
|
||||||
|
sendToConsole: false,
|
||||||
|
});
|
||||||
|
expect(consoleInfo).not.toHaveBeenCalled();
|
||||||
|
consoleInfo.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps production Node errors on stderr as well as sending them to Sentry", () => {
|
it("keeps production Node errors on stderr as well as sending them to Sentry", () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
|
|||||||
+4
-1
@@ -3,7 +3,7 @@ import { createLogger } from "../src/index.js";
|
|||||||
import { createSentrySink } from "../src/sentry.js";
|
import { createSentrySink } from "../src/sentry.js";
|
||||||
|
|
||||||
describe("createSentrySink", () => {
|
describe("createSentrySink", () => {
|
||||||
it("captures errors with namespace and structured context, but ignores warnings", () => {
|
it("captures errors and explicitly selected events with namespace and structured context", () => {
|
||||||
const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() };
|
const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() };
|
||||||
const sentry = {
|
const sentry = {
|
||||||
withScope: (callback: (value: typeof scope) => void) => callback(scope),
|
withScope: (callback: (value: typeof scope) => void) => callback(scope),
|
||||||
@@ -17,8 +17,11 @@ describe("createSentrySink", () => {
|
|||||||
});
|
});
|
||||||
const error = new Error("offline");
|
const error = new Error("offline");
|
||||||
logger.warn("ignored");
|
logger.warn("ignored");
|
||||||
|
logger.sentry.info("Cache warmed");
|
||||||
logger.error("Request failed", error, { requestId: "req_1" });
|
logger.error("Request failed", error, { requestId: "req_1" });
|
||||||
|
expect(sentry.captureMessage).toHaveBeenCalledWith("Cache warmed", "info");
|
||||||
expect(sentry.captureException).toHaveBeenCalledWith(error);
|
expect(sentry.captureException).toHaveBeenCalledWith(error);
|
||||||
|
expect(scope.setLevel).toHaveBeenCalledWith("info");
|
||||||
expect(scope.setTag).toHaveBeenCalledWith("logger.namespace", "API");
|
expect(scope.setTag).toHaveBeenCalledWith("logger.namespace", "API");
|
||||||
expect(scope.setExtras).toHaveBeenCalledWith({ context_0: { requestId: "req_1" } });
|
expect(scope.setExtras).toHaveBeenCalledWith({ context_0: { requestId: "req_1" } });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user