Skip to content

KvLogger config

  • Source: src/config/logging/KvLogger.ts

Usage

Import from the framework:

ts
import { KvLogger } from '@zintrust/core';

// Example (if supported by the module):
// KvLogger.*

Snapshot (top)

ts
/**
 * KV Logger
 * Writes batches of log events to a KV namespace (Cloudflare Workers compatible)
 *
 * Enabled via env:
 *  - KV_LOG_ENABLED (default: false)
 *  - KV_NAMESPACE (binding name; default: 'CACHE')
 *  - KV_LOG_RETENTION_DAYS (default: 30)
 */

import { Cloudflare } from '@zintrust/core';
import { Env } from '@zintrust/core';

export type KvLogEvent = {
  timestamp: string;
  level: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
  message: string;
  category?: string;
  data?: unknown;
  error?: string;
};

type KVNamespace = NonNullable\<ReturnType\<typeof Cloudflare.getKVBinding>>;

type PutOptions = { expiration?: number; expirationTtl?: number; metadata?: unknown };

const getRetentionTtlSeconds = (): number => {
  const days = Env.getInt('KV_LOG_RETENTION_DAYS', 30);
  const safeDays = Number.isFinite(days) && days > 0 ? days : 30;
  return safeDays * 24 * 60 * 60;
};

const getKvBindingName = (): string => {
  const name = Env.get('KV_NAMESPACE', 'CACHE').trim();
  return name.length > 0 ? name : 'CACHE';
};

const isEnabled = (): boolean => Env.getBool('KV_LOG_ENABLED', false);

const safeRandom = (): string => {
  try {
    // Prefer crypto if available
    const cryptoObj = (globalThis as unknown as { crypto?: Crypto }).crypto;
    if (cryptoObj?.getRandomValues) {
      const bytes = new Uint8Array(8);
      cryptoObj.getRandomValues(bytes);
      return Array.from(bytes)
        .map((b) => b.toString(16).padStart(2, '0'))
        .join('');
    }
  } catch {
    // fall through
  }
  const generate = Math.random().toString(16).slice(2); // NOSONAR
  const fallback = generate + Math.random().toString(16).slice(2); // NOSONAR this is not used for security
  return fallback;
};

const buildKey = (timestampIso: string): string => {
  const date = timestampIso.slice(0, 10);
  const hour = timestampIso.slice(11, 13);
  return `logs:${date}:${hour}:${safeRandom()}`;
};

let buffer: KvLogEvent[] = [];
let flushTimer: ReturnType\<typeof setTimeout> | undefined;
let flushPromise: Promise\<void> | undefined;

const scheduleFlush = async (): Promise\<void> => {
  if (flushPromise !== undefined) return flushPromise;

Snapshot (bottom)

ts
  await kv.put(key, payload, opts);
};

const flushNow = async (): Promise\<void> => {
  if (!isEnabled()) {
    buffer = [];
    return;
  }

  const kv = getKv();
  if (kv === null) {
    buffer = [];
    return;
  }

  const toSend = buffer;
  buffer = [];

  try {
    await putBatch(kv, toSend);
  } catch {
    // Best-effort: never throw from logging.
  }
};

const flushSoon = async (): Promise\<void> => {
  if (flushPromise !== undefined) return flushPromise;

  flushPromise = Promise.resolve()
    .then(async () => flushNow())
    .finally(() => {
      flushPromise = undefined;
    });

  return flushPromise;
};

export const KvLogger = Object.freeze({
  async enqueue(event: KvLogEvent): Promise\<void> {
    if (!isEnabled()) return Promise.resolve();

    buffer.push(event);

    // Basic size guard: flush if it gets too large
    const maxBatch = 100;
    if (buffer.length >= maxBatch) {
      // Cancel scheduled flush and flush immediately
      if (flushTimer !== undefined) {
        globalThis.clearTimeout(flushTimer);
        flushTimer = undefined;
      }
      return flushSoon();
    }

    return scheduleFlush();
  },
});

export default KvLogger;

Released under the MIT License.