Zum Inhalt springen
aviral gupta

// I5.5 · ca. 48 Min. · Aufbau

Projekt: ein Logdatei-Analysator

Sie bauen einen kleinen Logdatei-Analysator: Ein regulärer Ausdruck liest jede Zeile, eine Map zählt die Einträge nach Level und nach Stunde, und Intl schreibt die Zusammenfassung.

Lektion 5 von 5 in I5 Mit Daten arbeiten

Ende des Moduls

Danach können Sie

  • Logzeilen mit einem regulären Ausdruck mit benannten Gruppen lesen und Zeilen überspringen, die nicht passen
  • Einträge mit einer Map nach Level und nach UTC-Stunde zählen
  • Eine Zusammenfassung mit Intl schreiben, die auf jedem Computer gleich ausgegeben wird
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion I5.4. Was gibt das aus?

    console.log(new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1 }).format(1 / 6));
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. Wie viele dieser Zeilen akzeptiert das Muster?

    const LINE = /^(?<time>\S+) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;
    const lines = [
      "2026-10-05T08:15:02Z INFO server started",
      "2026-10-05T08:16:00Z DEBUG cache warm",
      "broken",
    ];
    console.log(lines.filter((line) => LINE.test(line)).length);
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie die Eigenschaft des Treffers, die die benannten Gruppen enthält.

    const { level } = LINE.exec(line).____;
    const { level } = LINE.exec(line).;
  4. Üben · Aufgabe 4 von 7

    Bringen Sie die Schritte des Analysators in die richtige Reihenfolge.

    1. 1.Die null-Werte entfernen
    2. 2.Die Einträge in einer Map zählen, nach Level und nach UTC-Stunde
    3. 3.Den Logtext in Zeilen teilen
    4. 4.Jede Zeile mit dem Muster lesen; eine schlechte Zeile ergibt null
    5. 5.Die Zähler mit Intl schreiben
  5. Üben · Aufgabe 5 von 7

    Zählen nach UTC-Stunde. Was gibt das aus?

    const times = ["2026-10-05T08:15:02Z", "2026-10-05T08:59:59Z", "2026-10-05T09:00:00Z"];
    const byHour = new Map();
    for (const t of times) {
      const hour = new Date(t).getUTCHours();
      byHour.set(hour, (byHour.get(hour) ?? 0) + 1);
    }
    console.log([...byHour].join(" | "));
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Jemand hat dem Muster das Flag g hinzugefügt. Wie viele Zeilen bestehen den Filter?

    const LINE = /^\S+ (INFO|WARN|ERROR) .+$/g;
    const lines = ["t1 INFO a", "t2 INFO b", "t3 INFO c"];
    console.log(lines.filter((line) => LINE.test(line)).length);
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe: Erweitern Sie den Analysator. Gruppieren Sie geparste Einträge mit dem has/set-Muster in einer Map nach Level, sodass jedes Level das Array seiner Einträge enthält. Geben Sie dann jeden ERROR-Eintrag mit seiner Uhrzeit aus, als Stunden:Minuten:Sekunden in UTC aus Intl.DateTimeFormat, gefolgt von seiner Meldung.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Der fertige Analysator

Das ganze Programm in einer Datei. LOG enthält sieben Zeilen, eine davon kaputt. parseLine() macht aus jeder Zeile ein Objekt oder null, countBy() zählt nach Level und nach UTC-Stunde, und formatSummary() schreibt die Anteile mit Intl auf Deutsch. Das Programm gibt den Datumsbereich, die Zusammenfassung, einen Balken pro Stunde und die verschiedenen Fehlermeldungen aus, die ein Set sammelt. Die Übungen bauen die drei Funktionen nacheinander.

main.js

const LOG = `2026-10-05T08:15:02Z INFO server started on port 8080
2026-10-05T08:15:09Z WARN config file missing, using defaults
2026-10-05T09:02:44Z INFO user ada logged in
this line is broken
2026-10-05T09:47:13Z ERROR database timeout after 5000 ms
2026-10-05T10:05:00Z INFO user bo logged in
2026-10-05T10:31:27Z ERROR database timeout after 5000 ms`;

const LINE = /^(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;

// One log line -> { time: Date, level, message }, or null if it does not match.
export function parseLine(line) {
  const match = LINE.exec(line);
  if (match === null) return null;
  const { time, level, message } = match.groups;
  return { time: new Date(time), level, message };
}

// Counts entries by the key that keyOf returns, in first-seen order.
export function countBy(entries, keyOf) {
  const counts = new Map();
  for (const entry of entries) {
    const key = keyOf(entry);
    counts.set(key, (counts.get(key) ?? 0) + 1);
  }
  return counts;
}

export const countByLevel = (entries) => countBy(entries, (entry) => entry.level);
export const countByHour = (entries) => countBy(entries, (entry) => entry.time.getUTCHours());

// counts: Map of level -> count. Returns the summary lines for a locale.
export function formatSummary(counts, locale) {
  const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
  const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 });
  const list = new Intl.ListFormat(locale, { type: "conjunction" });
  const lines = [];
  for (const [level, count] of counts) {
    lines.push(`${level}: ${count} (${percent.format(count / total)})`);
  }
  lines.push(`levels: ${list.format([...counts.keys()])}`);
  return lines;
}

const lines = LOG.split("\n");
const entries = lines.map(parseLine).filter((entry) => entry !== null);
console.log(`read ${entries.length} of ${lines.length} lines`);

const day = new Intl.DateTimeFormat("de-DE", { dateStyle: "long", timeZone: "UTC" });
const time = new Intl.DateTimeFormat("de-DE", { timeStyle: "short", timeZone: "UTC" });
console.log("Log vom", day.format(entries[0].time), "bis", time.format(entries.at(-1).time), "UTC");

for (const line of formatSummary(countByLevel(entries), "de-DE")) console.log(line);

for (const [hour, count] of countByHour(entries)) {
  console.log(`${String(hour).padStart(2, "0")}:00 UTC ${"#".repeat(count)}`);
}

const errors = new Set(entries.filter((entry) => entry.level === "ERROR").map((entry) => entry.message));
console.log("different errors:", errors.size, "-", [...errors].join("; "));

Ausführen mit

node main.js

Ausgabe

read 6 of 7 lines
Log vom 5. Oktober 2026 bis 10:31 UTC
INFO: 3 (50 %)
WARN: 1 (16,7 %)
ERROR: 2 (33,3 %)
levels: INFO, WARN und ERROR
08:00 UTC ##
09:00 UTC ##
10:00 UTC ##
different errors: 1 - database timeout after 5000 ms
  • Die kaputte Zeile wurde übersprungen: parseLine gab null zurück, und der Filter hat sie entfernt.
  • Die Level kamen in der Reihenfolge heraus, in der sie zuerst im Log standen, weil eine Map die Einfügereihenfolge behält.
  • Beide Fehler hatten dieselbe Meldung, das Set hat sie also nur einmal behalten.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 3

Eine Logzeile lesen

parseLine(line) bekommt eine Zeile wie "2026-10-05T09:47:13Z ERROR database timeout after 5000 ms" und gibt {time, level, message} zurück: time als Date, level als INFO, WARN oder ERROR und den ganzen Rest als message. Eine Zeile, die nicht passt, gibt null zurück. Der Startcode trennt an Leerzeichen, was die Meldung zerschneidet und nie null zurückgibt. Nutzen Sie ein verankertes Muster mit benannten Gruppen.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Drei benannte Gruppen, durch einzelne Leerzeichen getrennt: (?<time>…) (?<level>…) (?<message>…). Verankern Sie mit ^ und $.

  2. Hinweis 2

    Das Level ist eine Alternative: (?<level>INFO|WARN|ERROR). Die Meldung ist alles andere: (?<message>.+).

  3. Hinweis 3

    const match = LINE.exec(line); if (match === null) return null; dann match.groups lesen und time in new Date(…) einpacken.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

const LINE = /^(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;

// One log line -> { time: Date, level, message }, or null if it does not match.
export function parseLine(line) {
  const match = LINE.exec(line);
  if (match === null) return null;
  const { time, level, message } = match.groups;
  return { time: new Date(time), level, message };
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// One log line -> { time: Date, level, message }, or null if it does not match.
export function parseLine(line) {
  const [time, level, message] = line.split(" ");
  return { time: new Date(time), level, message };
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parseLine} from './main.js';

test('Der ganze Rest der Zeile ist die Meldung', () => {
  const entry = parseLine('2026-10-05T09:47:13Z ERROR database timeout after 5000 ms');
  assert.equal(entry?.level, 'ERROR', `das Level ist ${entry?.level}`);
  assert.equal(entry?.message, 'database timeout after 5000 ms', `die Meldung ist ${entry?.message}`);
});

test('Die Zeit ist ein Date zu diesem UTC-Moment', () => {
  const entry = parseLine('2026-10-05T08:15:02Z INFO server started');
  assert.ok(entry?.time instanceof Date, 'time sollte ein Date sein');
  assert.equal(entry.time.toISOString(), '2026-10-05T08:15:02.000Z', `die Zeit ist ${entry.time.toISOString()}`);
});

test('Eine kaputte Zeile ergibt null', () => {
  assert.equal(parseLine('this line is broken'), null, 'parseLine sollte für eine kaputte Zeile null zurückgeben');
});

test('Ein unbekanntes Level ergibt null', () => {
  assert.equal(parseLine('2026-10-05T08:16:00Z DEBUG cache warm'), null, 'DEBUG ist keines der drei Level');
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 3

Nach Level und nach Stunde zählen

countBy(entries, keyOf) soll eine Map von jedem Schlüssel, den keyOf zurückgibt, auf die Anzahl der Einträge mit diesem Schlüssel zurückgeben, in der Reihenfolge des ersten Auftretens. countByLevel und countByHour nutzen es, die zweite mit der UTC-Stunde von entry.time als Zahl. Das countBy im Startcode addiert 1 zu undefined, und countByHour nimmt das ganze Date als Schlüssel. Beheben Sie beides.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Die Zähler sind NaN. Was ist counts.get(key), wenn ein Schlüssel zum ersten Mal vorkommt?

  2. Hinweis 2

    Beginnen Sie bei 0: counts.set(key, (counts.get(key) ?? 0) + 1).

  3. Hinweis 3

    Der Schlüssel für eine Stunde ist entry.time.getUTCHours(), eine Zahl von 0 bis 23.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Counts entries by the key that keyOf returns, in first-seen order.
export function countBy(entries, keyOf) {
  const counts = new Map();
  for (const entry of entries) {
    const key = keyOf(entry);
    counts.set(key, (counts.get(key) ?? 0) + 1);
  }
  return counts;
}

export const countByLevel = (entries) => countBy(entries, (entry) => entry.level);
export const countByHour = (entries) => countBy(entries, (entry) => entry.time.getUTCHours());
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Counts entries by the key that keyOf returns, in first-seen order.
export function countBy(entries, keyOf) {
  const counts = new Map();
  for (const entry of entries) {
    const key = keyOf(entry);
    counts.set(key, counts.get(key) + 1);
  }
  return counts;
}

export const countByLevel = (entries) => countBy(entries, (entry) => entry.level);
// TODO: count by the hour of entry.time, in UTC.
export const countByHour = (entries) => countBy(entries, (entry) => entry.time);

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countByLevel, countByHour} from './main.js';

const at = (iso, level) => ({time: new Date(iso), level, message: 'x'});
const entries = [
  at('2026-10-05T08:15:02Z', 'INFO'),
  at('2026-10-05T08:40:00Z', 'ERROR'),
  at('2026-10-05T09:02:44Z', 'INFO'),
  at('2026-10-05T23:59:59Z', 'INFO')
];

test('Level werden in der Reihenfolge des ersten Auftretens gezählt', () => {
  const counts = countByLevel(entries);
  assert.deepEqual([...counts], [['INFO', 3], ['ERROR', 1]], `die Zähler sind ${JSON.stringify([...counts])}`);
});

test('Stunden sind UTC-Stunden als Zahlen', () => {
  const counts = countByHour(entries);
  assert.deepEqual([...counts], [[8, 2], [9, 1], [23, 1]], `die Zähler sind ${JSON.stringify([...counts])}`);
});

test('Keine Einträge ergeben eine leere Map', () => {
  assert.equal(countByLevel([]).size, 0, 'countByLevel([]) sollte leer sein');
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 3 von 3

Die Zusammenfassung mit Intl schreiben

formatSummary(counts, locale) bekommt eine Map von Level auf Anzahl und gibt ein Array von Zeilen zurück: eine Zeile "LEVEL: Anzahl (Anteil)" pro Level, mit dem Anteil als Prozentwert mit höchstens einer Nachkommastelle für das Locale, und dann "levels: " mit den Levelnamen als Liste für das Locale. Der Startcode baut Prozentwert und Liste von Hand, was weder für Deutsch noch für Englisch stimmt. Nutzen Sie Intl.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Der deutsche Anteil hat einen Punkt. Welches Intl-Werkzeug schreibt Prozentwerte für ein Locale?

  2. Hinweis 2

    new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }).format(count / total)

  3. Hinweis 3

    new Intl.ListFormat(locale, { type: "conjunction" }).format([...counts.keys()])

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// counts: Map of level -> count. Returns the summary lines for a locale.
export function formatSummary(counts, locale) {
  const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
  const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 });
  const list = new Intl.ListFormat(locale, { type: "conjunction" });
  const lines = [];
  for (const [level, count] of counts) {
    lines.push(`${level}: ${count} (${percent.format(count / total)})`);
  }
  lines.push(`levels: ${list.format([...counts.keys()])}`);
  return lines;
}
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// counts: Map of level -> count. Returns the summary lines for a locale.
export function formatSummary(counts, locale) {
  const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
  const lines = [];
  for (const [level, count] of counts) {
    lines.push(`${level}: ${count} (${Math.round((count / total) * 1000) / 10}%)`);
  }
  lines.push(`levels: ${[...counts.keys()].join(", ")}`);
  return lines;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatSummary} from './main.js';

const counts = new Map([['INFO', 3], ['WARN', 1], ['ERROR', 2]]);

test('Deutsche Anteile haben ein Komma und ein geschütztes Leerzeichen vor %', () => {
  const lines = formatSummary(counts, 'de-DE');
  assert.equal(lines[1], 'WARN: 1 (16,7\u00a0%)', `die WARN-Zeile ist ${JSON.stringify(lines[1])}`);
});

test('Englische Anteile haben einen Punkt und kein Leerzeichen', () => {
  const lines = formatSummary(counts, 'en-US');
  assert.equal(lines[2], 'ERROR: 2 (33.3%)', `die ERROR-Zeile ist ${JSON.stringify(lines[2])}`);
});

test('Die Level sind eine Liste in der Sprache des Locales', () => {
  const german = formatSummary(counts, 'de-DE').at(-1);
  const english = formatSummary(counts, 'en-US').at(-1);
  assert.equal(german, 'levels: INFO, WARN und ERROR', `die deutsche Listenzeile ist ${german}`);
  assert.equal(english, 'levels: INFO, WARN, and ERROR', `die englische Listenzeile ist ${english}`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

Zeilen, die nicht gepasst haben, nicht entfernen

const LINE = /^(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) (?<level>INFO|WARN|ERROR) (?<message>.+)$/;

// One log line -> { time: Date, level, message }, or null if it does not match.
function parseLine(line) {
  const match = LINE.exec(line);
  if (match === null) return null;
  const { time, level, message } = match.groups;
  return { time: new Date(time), level, message };
}

const lines = ["2026-10-05T08:15:02Z INFO started", "broken"];
const levels = lines.map(parseLine).map((entry) => entry.level);
console.log(levels);

Was Node.js ausgibt

TypeError: Cannot read properties of null (reading 'level')

Warum, und die Lösung

parseLine gibt für eine Zeile, die nicht passt, null zurück, und null hat keine Eigenschaften. Filtern Sie die null-Werte heraus, bevor Sie die Einträge benutzen: lines.map(parseLine).filter((entry) => entry !== null). Eine Logdatei hat fast immer eine kaputte oder leere Zeile, etwa die letzte Zeile nach dem letzten Zeilenumbruch.

matchAll über das ganze Log ohne g

const log = "INFO started\nERROR timeout";
const LINE = /^(?<level>INFO|WARN|ERROR) (?<message>.+)$/m;
for (const match of log.matchAll(LINE)) console.log(match.groups.level);

Was Node.js ausgibt

TypeError: String.prototype.matchAll called with a non-global RegExp argument

Warum, und die Lösung

Den ganzen Text mit matchAll auf einmal zu lesen ist in Ordnung, aber matchAll braucht das Flag g: /^…$/gm. Das Flag m lässt ^ und $ an jeder Zeile treffen, g lässt matchAll jeden Treffer finden. Für Ja/Nein-Prüfungen mit test lassen Sie g weg.

Den Zeit-String statt eines Dates formatieren

const time = new Intl.DateTimeFormat("en-GB", { timeStyle: "short", timeZone: "UTC" });
const match = /^(?<time>\S+)/.exec("2026-10-05T08:15:02Z INFO started");
console.log(time.format(match.groups.time));

Was Node.js ausgibt

RangeError: Invalid time value

Warum, und die Lösung

Gruppen sind immer Strings. DateTimeFormat.format() will ein Date oder einen Timestamp und macht aus einem String eine Zahl, die für einen ISO-String NaN ergibt. Wandeln Sie zuerst um: time.format(new Date(match.groups.time)). Wenn Sie das einmal in parseLine tun, sieht der Rest des Programms den String nie.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Einlesen, dann den Text vergessen

Jede Zeile sieht so aus: 2026-10-05T09:47:13Z ERROR database timeout. Ein verankertes Muster mit drei benannten Gruppen liest sie: time, level (INFO|WARN|ERROR) und message, der Rest der Zeile. parseLine() gibt {time, level, message} zurück, mit time schon als Date, oder null, wenn exec keinen Treffer findet. lines.map(parseLine).filter((entry) => entry !== null) behält nur die guten Zeilen, und ab dann arbeitet das Programm mit Objekten, nicht mit Text. Ein Trennen an Leerzeichen würde die Meldung in Wörter zerschneiden.

Eine Zählfunktion

countBy(entries, keyOf) ist das Zählmuster aus Lektion I5.1, verallgemeinert: Es ruft keyOf für jeden Eintrag auf und addiert 1 zu diesem Schlüssel in einer Map, ausgehend von (counts.get(key) ?? 0). countByLevel übergibt entry.level, countByHour übergibt entry.time.getUTCHours(). Die Schlüssel bleiben in der Reihenfolge, in der sie zuerst auftreten, bei einem Log also in zeitlicher Reihenfolge. Die Stunde muss eine UTC-Stunde sein: getHours() würde nach der Zeitzone des Computers gruppieren, auf dem der Analysator läuft.

Eine Zusammenfassung für Menschen

formatSummary(counts, locale) macht aus der Map Text. Intl.NumberFormat mit style: "percent" und maximumFractionDigits: 1 schreibt jeden Anteil, Intl.ListFormat verbindet die Levelnamen, und Intl.DateTimeFormat mit timeZone: "UTC" schreibt das Datum des Logs. Dieselbe Funktion schreibt "33,3 %" für de-DE und "33.3%" für en-US. Im Deutschen ist das Leerzeichen vor % ein geschütztes Leerzeichen, ein Test, der diesen Text erwartet, schreibt es also als \u00a0. Erzeugen Sie jeden Formatierer einmal und verwenden Sie ihn in der Schleife wieder.

Quellen

Zuletzt geprüft am 5. Oktober 2026