Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f18e0855e | ||
|
|
44f471653e | ||
|
|
0ccb7f911d | ||
|
|
e0b450157d | ||
|
|
cebe107034 | ||
|
|
47e3930b4b | ||
|
|
1c63e58809 | ||
|
|
d211d9f8bc | ||
|
|
afd2ca1978 | ||
|
|
934367d695 |
@@ -0,0 +1,57 @@
|
||||
# One-time bootstrap for the v0.9.1 tag, whose original tag workflow failed before publishing.
|
||||
when:
|
||||
- event: manual
|
||||
|
||||
steps:
|
||||
publish-v0-9-1:
|
||||
image: node:24-bookworm-slim
|
||||
environment:
|
||||
NPM_TOKEN:
|
||||
from_secret: gitea_package_token
|
||||
commands:
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@11.0.0 --activate
|
||||
- apt-get update && apt-get install --yes --no-install-recommends ca-certificates git
|
||||
- pnpm install --frozen-lockfile
|
||||
- test "$(node -p \"require('./package.json').version\")" = "0.9.1"
|
||||
- git rev-parse --verify refs/tags/v0.9.1
|
||||
- pnpm check
|
||||
- pnpm test
|
||||
- pnpm build
|
||||
- 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"
|
||||
- pnpm publish --no-git-checks
|
||||
|
||||
notify-bootstrap-failure:
|
||||
image: curlimages/curl:8.14.1
|
||||
depends_on: [publish-v0-9-1]
|
||||
environment:
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_pushes_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Package bootstrap failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl --fail --show-error --silent --request POST --header 'Content-Type: application/json' --header "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" --data "$BODY" "$MATTERMOST_POST_API_URL"
|
||||
when:
|
||||
- status: [failure]
|
||||
|
||||
notify-bootstrap-success:
|
||||
image: curlimages/curl:8.14.1
|
||||
depends_on: [publish-v0-9-1]
|
||||
environment:
|
||||
MATTERMOST_BOT_ACCESS_TOKEN:
|
||||
from_secret: mattermost_bot_access_token
|
||||
MATTERMOST_CHANNEL_ID:
|
||||
from_secret: mattermost_pushes_channel_id
|
||||
MATTERMOST_POST_API_URL:
|
||||
from_secret: mattermost_post_api_url
|
||||
commands:
|
||||
- |
|
||||
BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Package bootstrap success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||
curl --fail --show-error --silent --request POST --header 'Content-Type: application/json' --header "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" --data "$BODY" "$MATTERMOST_POST_API_URL"
|
||||
when:
|
||||
- status: [success]
|
||||
@@ -7,13 +7,18 @@ depends_on:
|
||||
- ci
|
||||
|
||||
clone:
|
||||
depth: 0
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
depth: 0
|
||||
partial: false
|
||||
tags: true
|
||||
|
||||
steps:
|
||||
release:
|
||||
image: node:24-bookworm-slim
|
||||
environment:
|
||||
GITEA_PACKAGE_TOKEN:
|
||||
NPM_TOKEN:
|
||||
from_secret: gitea_package_token
|
||||
GITEA_REGISTRY_USERNAME:
|
||||
from_secret: gitea_registry_username
|
||||
@@ -22,12 +27,14 @@ steps:
|
||||
commands:
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@11.0.0 --activate
|
||||
- apt-get update && apt-get install --yes --no-install-recommends ca-certificates git
|
||||
- pnpm install --frozen-lockfile
|
||||
- git config user.name "Woodpecker CI"
|
||||
- git config user.email "woodpecker-ci@mifi.dev"
|
||||
- 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 -- //git.mifi.dev/api/packages/mifi/npm/:_authToken "$GITEA_PACKAGE_TOKEN"
|
||||
- npm config set -- //git.mifi.dev/api/packages/mifi/npm/:_authToken "$NPM_TOKEN"
|
||||
- pnpm build
|
||||
- pnpm release
|
||||
|
||||
notify-release-failure:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# [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
|
||||
|
||||
- **release:** build package before publishing ([e0b4501](https://git.mifi.dev/mifi/logger/commit/e0b450157df7a8aa1ff8bf5299140b0e7b52a7f7))
|
||||
- **release:** build package before publishing ([cebe107](https://git.mifi.dev/mifi/logger/commit/cebe107034a15e9fd1517d872037e97337426b29))
|
||||
|
||||
## [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
|
||||
|
||||
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
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
@@ -30,6 +30,8 @@ import { createLogger } from "@mifi/logger";
|
||||
import { createSentrySink } from "@mifi/logger/sentry";
|
||||
|
||||
const logger = createLogger({ environment: "production", sentry: createSentrySink(Sentry) });
|
||||
|
||||
logger.sentry.info("Cache warmed", { entries: 42 });
|
||||
```
|
||||
|
||||
## Releases
|
||||
@@ -39,3 +41,5 @@ Woodpecker verifies pull requests and `main`, reports CI to Mattermost, then aut
|
||||
Use `fix:` for a patch, `feat:` for a minor release, and `!` or a `BREAKING CHANGE:` footer for a major release. Commits such as `docs:`, `test:`, and `chore:` do not release a package. Before enabling the automation, publish and tag the existing `v0.9.1` once as its baseline.
|
||||
|
||||
The release workflow uses the existing global `gitea_package_token`, `gitea_registry_username`, `gitea_release_token`, `mattermost_bot_access_token`, `mattermost_tests_channel_id`, `mattermost_pushes_channel_id`, and `mattermost_post_api_url` secrets.
|
||||
|
||||
The repository includes a guarded, one-time manual bootstrap workflow for `v0.9.1`; remove it after that initial package version has been published.
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mifi/logger",
|
||||
"version": "0.9.1",
|
||||
"version": "0.10.0",
|
||||
"description": "Intentional, namespaced logging for browser and Node.js TypeScript applications.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"prepack": "npm run build",
|
||||
"check": "tsc --noEmit",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
const releaseUsername = process.env.GITEA_REGISTRY_USERNAME;
|
||||
const releaseToken = process.env.GITEA_RELEASE_TOKEN;
|
||||
const repositoryUrl =
|
||||
releaseUsername && releaseToken
|
||||
? `https://${encodeURIComponent(releaseUsername)}:${encodeURIComponent(releaseToken)}@git.mifi.dev/mifi/logger.git`
|
||||
: "https://git.mifi.dev/mifi/logger.git";
|
||||
|
||||
module.exports = {
|
||||
branches: ["main"],
|
||||
repositoryUrl,
|
||||
tagFormat: "v${version}",
|
||||
plugins: [
|
||||
"@semantic-release/commit-analyzer",
|
||||
|
||||
+18
-2
@@ -97,13 +97,20 @@ export function createLogger(options: LoggerOptions): Logger {
|
||||
levelRank(level) >= levelRank(threshold) &&
|
||||
(!runtimeOverride?.namespaces.length ||
|
||||
namespaceMatches(namespace, runtimeOverride.namespaces));
|
||||
const emit = (level: Exclude<LogLevel, "silent">, arguments_: readonly unknown[]) => {
|
||||
if (!enabled(level)) return;
|
||||
const emit = (
|
||||
level: Exclude<LogLevel, "silent">,
|
||||
arguments_: readonly unknown[],
|
||||
sendToSentry = false,
|
||||
) => {
|
||||
const sendToConsole = enabled(level);
|
||||
if (!sendToConsole && !sendToSentry) return;
|
||||
const event: LogEvent = {
|
||||
level,
|
||||
namespace,
|
||||
arguments: evaluate(arguments_),
|
||||
timestamp: new Date(),
|
||||
sendToSentry,
|
||||
sendToConsole,
|
||||
};
|
||||
sinks.forEach((sink) => sink.emit(event));
|
||||
};
|
||||
@@ -123,6 +130,14 @@ export function createLogger(options: LoggerOptions): Logger {
|
||||
};
|
||||
const api: Logger = {
|
||||
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) =>
|
||||
createLogger({ ...options, namespace: [namespace, child].filter(Boolean) as string[] }),
|
||||
trace: (...items) => emit("trace", items),
|
||||
@@ -187,6 +202,7 @@ function supportsAnsi(): boolean {
|
||||
export function createConsoleSink(): LogSink {
|
||||
return {
|
||||
emit: (event) => {
|
||||
if (!event.sendToConsole) return;
|
||||
const method = event.level === "trace" ? "debug" : event.level;
|
||||
const console_ = globalThis.console as Console & Record<string, unknown>;
|
||||
const fn = console_[method];
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ export interface SentryLike {
|
||||
export function createSentrySink(sentry: SentryLike): LogSink {
|
||||
return {
|
||||
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 extras = Object.fromEntries(
|
||||
event.arguments
|
||||
@@ -26,11 +26,11 @@ export function createSentrySink(sentry: SentryLike): LogSink {
|
||||
event.arguments.filter((item) => typeof item === "string").join(" ") ||
|
||||
"Logger error";
|
||||
sentry.withScope((scope) => {
|
||||
scope.setLevel("error");
|
||||
scope.setLevel(event.level);
|
||||
if (event.namespace) scope.setTag("logger.namespace", event.namespace);
|
||||
scope.setExtras(extras);
|
||||
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;
|
||||
arguments: readonly unknown[];
|
||||
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. */
|
||||
@@ -33,8 +37,20 @@ export interface LoggerOptions {
|
||||
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 {
|
||||
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;
|
||||
trace(...arguments_: readonly unknown[]): void;
|
||||
debug(...arguments_: readonly unknown[]): void;
|
||||
|
||||
@@ -78,6 +78,26 @@ describe("createLogger", () => {
|
||||
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", () => {
|
||||
const destination = sink();
|
||||
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";
|
||||
|
||||
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 sentry = {
|
||||
withScope: (callback: (value: typeof scope) => void) => callback(scope),
|
||||
@@ -17,8 +17,11 @@ describe("createSentrySink", () => {
|
||||
});
|
||||
const error = new Error("offline");
|
||||
logger.warn("ignored");
|
||||
logger.sentry.info("Cache warmed");
|
||||
logger.error("Request failed", error, { requestId: "req_1" });
|
||||
expect(sentry.captureMessage).toHaveBeenCalledWith("Cache warmed", "info");
|
||||
expect(sentry.captureException).toHaveBeenCalledWith(error);
|
||||
expect(scope.setLevel).toHaveBeenCalledWith("info");
|
||||
expect(scope.setTag).toHaveBeenCalledWith("logger.namespace", "API");
|
||||
expect(scope.setExtras).toHaveBeenCalledWith({ context_0: { requestId: "req_1" } });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user