tuil
ReferencePackages@mwillbanks/tuil-loggingAPI

LogPipeline

class exported by @mwillbanks/tuil-logging.

View rawEdit

class

Public class exported by @mwillbanks/tuil-logging.

export class LogPipeline {
  readonly #parsers: readonly LogParser[];
  readonly #enrichers: readonly LogEnricher[];
  readonly #redactors: readonly LogRedactor[];
  readonly buffer: LogRingBuffer;
  readonly #history: string[] = [];
  readonly #saved = new Map<string, string>();
  readonly #deduplicate: boolean;
  readonly #sampleEvery: number;
  readonly #maxPerSecond?: number;
  readonly #queryHistoryLimit: number;
  readonly #now: () => number;
  #ingested = 0;
  #rateWindow = { second: -1, count: 0 };

  constructor(
    options: {
      readonly parsers?: readonly LogParser[];
      readonly enrichers?: readonly LogEnricher[];
      readonly redactors?: readonly LogRedactor[];
      readonly capacity?: number;
      readonly deduplicate?: boolean;
      readonly sampleEvery?: number;
      readonly maxPerSecond?: number;
      readonly queryHistoryLimit?: number;
      readonly now?: () => number;
    } = {},
  ) {
    this.#parsers = Object.freeze([...(options.parsers ?? builtInLogParsers)]);
    this.#enrichers = Object.freeze([...(options.enrichers ?? [])]);
    this.#redactors = Object.freeze([...(options.redactors ?? [])]);
    this.buffer = new LogRingBuffer(options.capacity);
    this.#deduplicate = options.deduplicate ?? false;
    this.#sampleEvery = positiveLogCount(
      "sampleEvery",
      options.sampleEvery ?? 1,
    );
    this.#maxPerSecond =
      options.maxPerSecond === undefined
        ? undefined
        : positiveLogCount("maxPerSecond", options.maxPerSecond);
    this.#queryHistoryLimit = positiveLogCount(
      "queryHistoryLimit",
      options.queryHistoryLimit ?? 100,
    );
    this.#now = options.now ?? Date.now;
  }

  ingest(input: string, parserId?: string): readonly LogRecord[] {
    const parser = parserId
      ? this.#parsers.find((item) => item.id === parserId)
      : this.#parsers.toSorted(
          (left, right) => right.detect(input) - left.detect(input),
        )[0];
    if (!parser) {
      throw new Error(`Log parser "${parserId ?? "detected"}" is unavailable`);
    }
    const output = parser.parse(input).map((item) => this.#ingestRecord(item));
    return Object.freeze(output);
  }

  #ingestRecord(item: LogRecord): LogRecord {
    const transformed = this.#applyPipeline(item);
    this.#ingested += 1;
    if (this.#ingested % this.#sampleEvery !== 0) {
      this.buffer.recordSampled();
      return record({ ...transformed, sampled: true });
    }
    const duplicate = this.#deduplicatedRecord(transformed);
    if (duplicate) return duplicate;
    const rateLimited = this.#applyRateLimit(transformed);
    if (rateLimited.rateLimited) {
      this.buffer.recordRateLimited();
      return rateLimited;
    }
    this.buffer.push(rateLimited);
    return rateLimited;
  }

  #applyPipeline(item: LogRecord): LogRecord {
    let transformed = item;
    for (const enricher of this.#enrichers)
      transformed = enricher.enrich(transformed);
    for (const redactor of this.#redactors)
      transformed = redactor.redact(transformed);
    return record({ ...transformed });
  }

  #applyRateLimit(item: LogRecord): LogRecord {
    const second = Math.floor(this.#now() / 1_000);
    if (this.#rateWindow.second !== second) {
      this.#rateWindow = { second, count: 0 };
    }
    this.#rateWindow.count += 1;
    return this.#maxPerSecond !== undefined &&
      this.#rateWindow.count > this.#maxPerSecond
      ? record({ ...item, rateLimited: true })
      : item;
  }

  #deduplicatedRecord(item: LogRecord): LogRecord | undefined {
    const previous = this.buffer.last();
    if (!this.#deduplicate || !previous || !sameLogRecord(previous, item))
      return undefined;
    const duplicate = record({
      ...previous,
      duplicateCount: (previous.duplicateCount ?? 1) + 1,
    });
    this.buffer.replaceLast(duplicate);
    return duplicate;
  }

  query(source: string): readonly LogRecord[] {
    this.#history.push(source);
    if (this.#history.length > this.#queryHistoryLimit) this.#history.shift();
    return this.filter(source);
  }

  filter(source: string): readonly LogRecord[] {
    const query = compileLogQuery(source);
    if (query.diagnostics.length > 0) return [];
    return Object.freeze(this.buffer.records().filter(query.predicate));
  }

  saveSearch(name: string, source: string): void {
    this.#saved.set(name, source);
  }

  savedSearches(): Readonly<Record<string, string>> {
    return Object.freeze(Object.fromEntries(this.#saved));
  }

  history(): readonly string[] {
    return Object.freeze([...this.#history]);
  }

  clear(): void {
    this.#ingested = 0;
    this.#rateWindow = { second: -1, count: 0 };
    this.buffer.clear();
  }

  replay(records: readonly LogRecord[]): void {
    for (const item of records) this.buffer.push(this.#applyPipeline(item));
  }

  export(
    records: readonly LogRecord[] = this.buffer.records(),
    format: "jsonl" | "text" = "jsonl",
  ): string {
    if (format === "text") {
      return records.map((item) => String(item.body)).join("\n");
    }
    return records
      .map((item) =>
        JSON.stringify(item, (_key, value) =>
          typeof value === "bigint" ? value.toString() : value,
        ),
      )
      .join("\n");
  }
}

Members

MemberTypeRequiredDescriptionRelated types
#parsersreadonly LogParser[]YesThe #parsers member uses the readonly LogParser[] contract.LogParser
#enrichersreadonly LogEnricher[]YesThe #enrichers member uses the readonly LogEnricher[] contract.LogEnricher
#redactorsreadonly LogRedactor[]YesThe #redactors member uses the readonly LogRedactor[] contract.LogRedactor
bufferLogRingBufferYesThe buffer member uses the LogRingBuffer contract.
#historystring[]YesThe #history member uses the string[] contract.
#savedMap<string, string>YesThe #saved member uses the Map<string, string> contract.
#deduplicatebooleanYesThe #deduplicate member uses the boolean contract.
#sampleEverynumberYesThe #sampleEvery member uses the number contract.
#maxPerSecondnumber | undefinedNoThe #maxPerSecond member uses the number | undefined contract.
#queryHistoryLimitnumberYesThe #queryHistoryLimit member uses the number contract.
#now() => numberYesThe #now member uses the () => number contract.
#ingestednumberYesThe #ingested member uses the number contract.
#rateWindow&#123; second: number; count: number; &#125;YesThe #rateWindow member uses the &#123; second: number; count: number; &#125; contract.
__constructoranyYesThe __constructor member uses the any contract.
ingest(input: string, parserId?: string) => readonly LogRecord[]YesThe ingest member uses the (input: string, parserId?: string) => readonly LogRecord[] contract.LogRecord
#ingestRecord(item: LogRecord) => LogRecordYesThe #ingestRecord member uses the (item: LogRecord) => LogRecord contract.LogRecord
#applyPipeline(item: LogRecord) => LogRecordYesThe #applyPipeline member uses the (item: LogRecord) => LogRecord contract.LogRecord
#applyRateLimit(item: LogRecord) => LogRecordYesThe #applyRateLimit member uses the (item: LogRecord) => LogRecord contract.LogRecord
#deduplicatedRecord(item: LogRecord) => LogRecord | undefinedYesThe #deduplicatedRecord member uses the (item: LogRecord) => LogRecord | undefined contract.LogRecord
query(source: string) => readonly LogRecord[]YesThe query member uses the (source: string) => readonly LogRecord[] contract.LogRecord
filter(source: string) => readonly LogRecord[]YesThe filter member uses the (source: string) => readonly LogRecord[] contract.LogRecord
saveSearch(name: string, source: string) => voidYesThe saveSearch member uses the (name: string, source: string) => void contract.
savedSearches() => Readonly<Record<string, string>>YesThe savedSearches member uses the () => Readonly<Record<string, string>> contract.
history() => readonly string[]YesThe history member uses the () => readonly string[] contract.
clear() => voidYesThe clear member uses the () => void contract.
replay(records: readonly LogRecord[]) => voidYesThe replay member uses the (records: readonly LogRecord[]) => void contract.LogRecord
export(records?: readonly LogRecord[], format?: "jsonl" | "text") => stringYesThe export member uses the (records?: readonly LogRecord[], format?: "jsonl" | "text") => string contract.LogRecord

Parameters

This declaration has no public members.

Returns

This declaration does not return a value.

Throws

No thrown errors are documented for this declaration.

Source

View the secondary source reference

Package

@mwillbanks/tuil-logging

On this page