@sentio/runtime 4.5.0-rc.2 → 4.5.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15202,21 +15202,153 @@ function stringify(obj) {
15202
15202
  return value;
15203
15203
  });
15204
15204
  }
15205
- function setupLogger(json, enableDebug, workerId) {
15205
+ const DEFAULT_LOG_GUARD_OPTIONS = {
15206
+ maxLineBytes: 16 * 1024,
15207
+ maxLinesPerSecond: 1e3,
15208
+ maxBytesPerSecond: 1024 * 1024
15209
+ };
15210
+ const LOG_GUARD_ENV = {
15211
+ maxLineBytes: "SENTIO_LOG_MAX_LINE_BYTES",
15212
+ maxLinesPerSecond: "SENTIO_LOG_MAX_LINES_PER_SECOND",
15213
+ maxBytesPerSecond: "SENTIO_LOG_MAX_BYTES_PER_SECOND"
15214
+ };
15215
+ /** Reads the guard limits from the environment, falling back to the defaults for unset or invalid values. */
15216
+ function logGuardOptionsFromEnv(env = process.env) {
15217
+ const read = (key) => {
15218
+ const raw = env[LOG_GUARD_ENV[key]]?.trim();
15219
+ if (!raw) return DEFAULT_LOG_GUARD_OPTIONS[key];
15220
+ const value = Number(raw);
15221
+ return Number.isSafeInteger(value) && value >= 0 ? value : DEFAULT_LOG_GUARD_OPTIONS[key];
15222
+ };
15223
+ return {
15224
+ maxLineBytes: read("maxLineBytes"),
15225
+ maxLinesPerSecond: read("maxLinesPerSecond"),
15226
+ maxBytesPerSecond: read("maxBytesPerSecond")
15227
+ };
15228
+ }
15229
+ const WINDOW_MS = 1e3;
15230
+ /**
15231
+ * Per-process accounting behind {@link LogGuardOptions}: truncates oversized text and decides,
15232
+ * per one-second window, whether another line still fits. Lines that do not fit are dropped;
15233
+ * once the window ends a single summary line reports how many were lost, via `onSummary`.
15234
+ */
15235
+ var LogGuard = class {
15236
+ options;
15237
+ onSummary;
15238
+ now;
15239
+ windowStart = -Infinity;
15240
+ lines = 0;
15241
+ bytes = 0;
15242
+ droppedLines = 0;
15243
+ droppedBytes = 0;
15244
+ summaryTimer;
15245
+ constructor(options, onSummary, now = Date.now) {
15246
+ this.options = options;
15247
+ this.onSummary = onSummary;
15248
+ this.now = now;
15249
+ }
15250
+ /** Returns `text` unchanged when it fits in `maxLineBytes`, otherwise its head plus a notice. */
15251
+ truncate(text) {
15252
+ const max = this.options.maxLineBytes;
15253
+ if (max <= 0 || text.length * 3 <= max) return text;
15254
+ const size = Buffer.byteLength(text);
15255
+ if (size <= max) return text;
15256
+ return `${new TextDecoder().decode(Buffer.from(text).subarray(0, max), { stream: true })} ...[truncated by sentio runtime: line was ${size} bytes, limit ${max}]`;
15257
+ }
15258
+ /** Accounts a line of `size` bytes against the current window; `false` means the line must be dropped. */
15259
+ admit(size) {
15260
+ const t = this.now();
15261
+ if (t - this.windowStart >= WINDOW_MS) this.rollWindow(t);
15262
+ const { maxLinesPerSecond, maxBytesPerSecond } = this.options;
15263
+ const overLines = maxLinesPerSecond > 0 && this.lines + 1 > maxLinesPerSecond;
15264
+ const overBytes = maxBytesPerSecond > 0 && this.bytes + size > maxBytesPerSecond;
15265
+ if (overLines || overBytes) {
15266
+ this.droppedLines++;
15267
+ this.droppedBytes += size;
15268
+ this.scheduleSummary(this.windowStart + WINDOW_MS - t);
15269
+ return false;
15270
+ }
15271
+ this.lines++;
15272
+ this.bytes += size;
15273
+ return true;
15274
+ }
15275
+ /** Emits the pending drop summary, if any, without waiting for the window to end. */
15276
+ flush() {
15277
+ if (this.summaryTimer) {
15278
+ clearTimeout(this.summaryTimer);
15279
+ this.summaryTimer = void 0;
15280
+ }
15281
+ if (this.droppedLines === 0) return;
15282
+ const { droppedLines, droppedBytes } = this;
15283
+ this.droppedLines = 0;
15284
+ this.droppedBytes = 0;
15285
+ const { maxLinesPerSecond, maxBytesPerSecond } = this.options;
15286
+ this.onSummary(`[sentio runtime] dropped ${droppedLines} console log lines (${droppedBytes} bytes) in the last second: output rate limit exceeded (max ${maxLinesPerSecond} lines/s, ${maxBytesPerSecond} bytes/s)`);
15287
+ }
15288
+ rollWindow(t) {
15289
+ this.flush();
15290
+ this.windowStart = t;
15291
+ this.lines = 0;
15292
+ this.bytes = 0;
15293
+ }
15294
+ scheduleSummary(delayMs) {
15295
+ if (this.summaryTimer) return;
15296
+ this.summaryTimer = setTimeout(() => {
15297
+ this.summaryTimer = void 0;
15298
+ this.flush();
15299
+ }, Math.max(delayMs, 0));
15300
+ this.summaryTimer.unref?.();
15301
+ }
15302
+ };
15303
+ const GUARD_BYPASS = Symbol.for("sentio.logGuardBypass");
15304
+ const MESSAGE = Symbol.for("message");
15305
+ const OWN_INFO_FIELDS = /* @__PURE__ */ new Set(["level", "timestamp"]);
15306
+ function setupLogger(json, enableDebug, workerId, guardOptions = logGuardOptionsFromEnv()) {
15307
+ const utilFormatter = { transform: (info) => {
15308
+ const stringRes = [];
15309
+ if (typeof info.message === "object") stringRes.push(stringify(info.message));
15310
+ else stringRes.push(info.message);
15311
+ const args = info[Symbol.for("splat")];
15312
+ if (args) for (const idx in args) {
15313
+ const arg = args[idx];
15314
+ if (typeof arg === "object") stringRes.push(stringify(arg));
15315
+ else stringRes.push(arg);
15316
+ }
15317
+ info.message = stringRes.join(" ");
15318
+ return info;
15319
+ } };
15320
+ const guard = new LogGuard(guardOptions, (summary) => {
15321
+ logger.warn({
15322
+ message: summary,
15323
+ [GUARD_BYPASS]: true
15324
+ });
15325
+ });
15326
+ const render = json ? import_winston.format.json() : import_winston.format.simple();
15206
15327
  const logger = (0, import_winston.createLogger)({
15207
- format: import_winston.format.combine(import_winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), { transform: (info) => {
15208
- const stringRes = [];
15209
- if (typeof info.message === "object") stringRes.push(stringify(info.message));
15210
- else stringRes.push(info.message);
15211
- const args = info[Symbol.for("splat")];
15212
- if (args) for (const idx in args) {
15213
- const arg = args[idx];
15214
- if (typeof arg === "object") stringRes.push(stringify(arg));
15215
- else stringRes.push(arg);
15216
- }
15217
- info.message = stringRes.join(" ");
15218
- return info;
15219
- } }, import_winston.format.errors({ stack: true }), json ? import_winston.format.json() : import_winston.format.simple(), import_winston.format.label({ label: workerId ? `worker #{workerId}` : "" })),
15328
+ format: import_winston.format.combine(import_winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), utilFormatter, import_winston.format.errors({ stack: true }), render, { transform: (info) => {
15329
+ if (info[GUARD_BYPASS] || info.level === "debug" && !enableDebug) return info;
15330
+ let size = Buffer.byteLength(String(info[MESSAGE]));
15331
+ const max = guardOptions.maxLineBytes;
15332
+ if (max > 0 && size > max) {
15333
+ let dropped = 0;
15334
+ for (const key of Object.keys(info)) {
15335
+ const keepAsText = (key === "message" || key === "stack") && typeof info[key] === "string";
15336
+ if (!OWN_INFO_FIELDS.has(key) && !keepAsText) {
15337
+ delete info[key];
15338
+ dropped++;
15339
+ }
15340
+ }
15341
+ info.message = guard.truncate(String(info.message ?? ""));
15342
+ if (typeof info.stack === "string") info.stack = guard.truncate(info.stack);
15343
+ if (dropped > 0) {
15344
+ const note = `${dropped} metadata fields dropped by sentio runtime: line was ${size} bytes, limit ${max}`;
15345
+ info.message += ` ...[${note}]`;
15346
+ }
15347
+ info = render.transform(info, render.options);
15348
+ size = Buffer.byteLength(String(info[MESSAGE]));
15349
+ }
15350
+ return guard.admit(size) ? info : false;
15351
+ } }, import_winston.format.label({ label: workerId ? `worker #{workerId}` : "" })),
15220
15352
  level: enableDebug ? "debug" : "info",
15221
15353
  transports: [new import_winston.transports.Console()]
15222
15354
  });