javascript 131 lines · 3 tabs

Split a Large Log File Into Date-Based Chunks With a Node.js Transform Stream

Shared by codesnips Aug 2026
3 tabs
'use strict';

const { Transform } = require('stream');

class LineSplitter extends Transform {
  constructor(options = {}) {
    super({ ...options, readableObjectMode: true });
    this.tail = '';
    this.lineNo = 0;
  }

  _transform(chunk, _enc, cb) {
    const data = this.tail + chunk.toString('utf8');
    const lines = data.split('\n');
    // Last element is a partial line (or '') until the next chunk arrives.
    this.tail = lines.pop();

    for (const text of lines) {
      this.push({ lineNo: ++this.lineNo, text, bytes: Buffer.byteLength(text) + 1 });
    }
    cb();
  }

  _flush(cb) {
    if (this.tail.length > 0) {
      this.push({ lineNo: ++this.lineNo, text: this.tail, bytes: Buffer.byteLength(this.tail) });
    }
    cb();
  }
}

module.exports = { LineSplitter };
3 files · javascript Explain with highlit

This snippet shows how to slice a huge, multi-gigabyte log file into separate files partitioned by day, without ever loading the whole thing into memory. The core idea is a streaming pipeline: bytes flow through a line splitter, then through a Transform that groups records by their date prefix and writes each group to a per-day file handle. Because everything is a stream, memory stays flat regardless of input size, and Node's built-in backpressure keeps the reader from outrunning the slower disk writes.

In line-splitter.js, LineSplitter extends Transform and buffers partial data across chunk boundaries. A single fixed-size read almost never lands on a newline, so the leftover tail after the last \n is stashed in this.tail and prepended to the next chunk. On _flush, any final line without a trailing newline is emitted so no record is silently dropped. It pushes objects rather than strings so downstream stages can keep metadata alongside the text.

date-chunker.js is where the routing happens. DateChunker extends Transform in objectMode and, for each line, extracts the leading YYYY-MM-DD via a regex. Lines that fail to parse are attributed to the current date so continuation lines and stack traces stay with their event rather than being discarded. writeStreamFor lazily opens a WriteStream per date and caches it in a Map, so a file is only created when a matching line appears. Crucially, when dest.write() returns false the chunker awaits the stream's drain event before continuing — this is the manual backpressure that prevents an unbounded number of buffered writes.

split-logs.js wires the stages together with stream.pipeline, which propagates errors and cleans up every stream on failure — a common pitfall when chaining .pipe() calls manually. After the pipeline resolves, it awaits chunker.closeAll() to flush and close every open file handle and reports the byte totals per day.

This pattern is the right tool when input is too large for memory, when output must be partitioned, and when ordering within each partition must be preserved. The main trade-offs are holding one open file descriptor per active date and relying on well-formed date prefixes, so the fallback-to-current-date rule matters for real-world messy logs.


Related snips

typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
typescript
import type { IncomingMessage, ServerResponse } from "http";

const MIN_BYTES = 1024;

const INCOMPRESSIBLE = /^(image|video|audio)\/|application\/(zip|gzip|x-brotli|pdf|octet-stream)/i;

Response compression (only when it helps)

performance http express
by codesnips 3 tabs
typescript
import pino, { Logger } from 'pino';
import { AsyncLocalStorage } from 'node:async_hooks';

export interface Store {
  requestId: string;
  logger: Logger;

Request ID + structured logging (Express + pino)

express logging observability
by codesnips 3 tabs
typescript
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import {

OpenTelemetry tracing for Node HTTP

observability tracing opentelemetry
by codesnips 3 tabs
ruby
class ReindexCheckpoint < ApplicationRecord
  enum status: { idle: 0, running: 1, done: 2, failed: 3 }

  validates :index_name, presence: true, uniqueness: true

  def self.for(index_name)

Safer Background Reindex: slice batches + checkpoints

rails reliability elasticsearch
by codesnips 4 tabs
typescript
import { onCLS, onINP, onLCP, onFCP, onTTFB, type Metric } from 'web-vitals';

export interface VitalPayload {
  id: string;
  name: Metric['name'];
  value: number;

Web Vitals reporting to an API endpoint

performance observability react
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Split a Large Log File Into Date-Based Chunks With a Node.js Transform Stream — share card
Link copied