mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
Frontend (@sentry/vue, loaded via build-time-gated dynamic import — no VITE_SENTRY_DSN_FRONTEND, no Sentry in the bundle; init joins the mount gate so instrumentation precedes the first render): - uncaught errors, route-change tracing / Web Vitals, error-only masked Session Replay, console.error capture fingerprinted per message prefix so each fallback source stays a distinct issue - explicit ip-source:exhausted signal via the app-events bus - hidden source maps uploaded (then deleted) by @sentry/vite-plugin, gated on SENTRY_AUTH_TOKEN Backend (@sentry/node, bootstrapped by sentry-instrument.js via node --import so ESM loader hooks land before express; never loaded without SENTRY_DSN_BACKEND): - Express error capture + per-route tracing - logger hook mirrors warn+ lines to Sentry Logs and elevates error+ to grouped, alertable Issues - Sentry Crons check-ins for the MaxMind / CAIDA / service-status jobs via the no-op-without-DSN wrapper common/sentry-cron.js Delivery & ops: - first-party tunnel /api/monitoring relays envelopes past ad blockers; DSN-allowlisted (never an open relay), exempt from the shared rate limiter with its own generous ceiling - rate-limit ledger file becomes opt-in now that the warn line reaches Sentry Logs; ecosystem.config.cjs carries the --import flag for pm2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.5 KiB
JavaScript
113 lines
4.5 KiB
JavaScript
// common/logger.js — shared pino logger for all backend code.
|
|
//
|
|
// LOG_LEVEL defaults to 'warn'; LOG_FORMAT=json switches off pino-pretty
|
|
// for log shippers. No NODE_ENV dependency.
|
|
//
|
|
// When SENTRY_DSN_BACKEND is set, the logMethod hook below mirrors log lines
|
|
// to Sentry (no official pino transport exists; the hook runs in-process and
|
|
// needs no extra dependency), split by severity to match the frontend's
|
|
// signal philosophy:
|
|
// - warn+ → Sentry Logs (queryable stream, Explore → Logs)
|
|
// - error+ → ALSO a Sentry Issue (grouped by the `err` stack when present,
|
|
// alertable) — a caught-and-logged upstream failure is a signal,
|
|
// not just a log line
|
|
// Without the DSN, @sentry/node is never loaded and the hook is not
|
|
// installed at all — the usual gating philosophy.
|
|
//
|
|
// ES module imports are hoisted, so backend-server.js's dotenv.config()
|
|
// runs after this file — we load .env ourselves here so LOG_LEVEL is
|
|
// honored. dotenv is idempotent; `quiet: true` avoids double banners.
|
|
|
|
import dotenv from 'dotenv';
|
|
import pino from 'pino';
|
|
|
|
dotenv.config({ quiet: true });
|
|
|
|
const useJson = process.env.LOG_FORMAT === 'json';
|
|
|
|
let sentry = null;
|
|
if (process.env.SENTRY_DSN_BACKEND) {
|
|
sentry = await import('@sentry/node');
|
|
}
|
|
|
|
// Flatten a pino context object into Sentry log attributes, which must be
|
|
// primitives: Errors become message + stack strings, nested objects become
|
|
// JSON. Exported for tests.
|
|
export const flattenLogAttributes = (ctx) => {
|
|
const attributes = {};
|
|
for (const [key, value] of Object.entries(ctx)) {
|
|
if (value instanceof Error) {
|
|
attributes[`${key}.message`] = value.message;
|
|
attributes[`${key}.stack`] = value.stack ?? '';
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
try {
|
|
attributes[key] = JSON.stringify(value);
|
|
} catch {
|
|
attributes[key] = String(value);
|
|
}
|
|
} else {
|
|
attributes[key] = value;
|
|
}
|
|
}
|
|
return attributes;
|
|
};
|
|
|
|
// pino numeric level → Sentry log level; anything below warn stays local.
|
|
const SENTRY_LOG_LEVELS = { 40: 'warn', 50: 'error', 60: 'fatal' };
|
|
|
|
const logger = pino({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
...(sentry ? {
|
|
hooks: {
|
|
// Mirror warn+ lines to Sentry Logs and elevate error+ to Issues,
|
|
// then log normally. pino call shapes are (msg) or (ctx, msg).
|
|
// Only enabled levels reach this hook, so LOG_LEVEL filtering
|
|
// applies to Sentry too.
|
|
logMethod(args, method, level) {
|
|
const sentryLevel = SENTRY_LOG_LEVELS[level];
|
|
if (sentryLevel) {
|
|
try {
|
|
const [first, second] = args;
|
|
const msg = String(typeof first === 'string' ? first : (second ?? ''));
|
|
const ctx = (typeof first === 'object' && first !== null) ? first : {};
|
|
const attributes = flattenLogAttributes(ctx);
|
|
sentry.logger[sentryLevel](msg, attributes);
|
|
if (level >= 50) {
|
|
// Group by the original throw site when an err
|
|
// object is attached (the logger.error({ err }...)
|
|
// convention); the log message rides along as a
|
|
// searchable tag.
|
|
const captureContext = {
|
|
level: sentryLevel,
|
|
tags: { log_message: msg },
|
|
extra: attributes,
|
|
};
|
|
if (ctx.err instanceof Error) {
|
|
sentry.captureException(ctx.err, captureContext);
|
|
} else {
|
|
sentry.captureMessage(msg, captureContext);
|
|
}
|
|
}
|
|
} catch {
|
|
// Telemetry must never break local logging.
|
|
}
|
|
}
|
|
return method.apply(this, args);
|
|
},
|
|
},
|
|
} : {}),
|
|
...(useJson ? {} : {
|
|
transport: {
|
|
target: 'pino-pretty',
|
|
options: {
|
|
colorize: true,
|
|
translateTime: 'HH:MM:ss.l',
|
|
ignore: 'pid,hostname',
|
|
singleLine: true,
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
export default logger;
|