From 658928fb29a1386f65be7d6854c74aed0099d098 Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 14 Oct 2026 18:04:25 +1100 Subject: [PATCH 1/3] fix: vendor drain3's template miner and drop loguru drain3's only release is a 2022 sdist with no wheel, pinned to jsonpickle 1.5.1 and cachetools 4.2.1, so a wheel-only install of logreducer fails. The Drain miner now lives in logreducer.template_mining, derived from Drain3 at 0fb6d6e (MIT), keeping only the streaming path logreducer drives. A committed golden of drain3 0.9.11's output on all 11 test corpora under five configs holds it to the same cluster per line, templates, counts and order. loguru, last released 2024-12, is replaced by stdlib logging under the logreducer logger. JSON log lines take scalo's keys and the file sink keeps seven 10 MB rotations. Runtime deps are now psutil, numpy, scikit-learn and typer, and every floor moves to the locked version. --- README.md | 12 +- pyproject.toml | 22 +- src/logreducer/anomaly.py | 4 +- src/logreducer/config.py | 4 +- src/logreducer/logging_config.py | 219 +++++---- src/logreducer/patterns.py | 62 ++- src/logreducer/sql.py | 2 +- src/logreducer/template_mining.py | 452 ++++++++++++++++++ src/logreducer/temporal.py | 28 +- .../integration/test_template_equivalence.py | 57 +++ tests/testdata/README.md | 4 + tests/testdata/template_mining_golden.json.gz | Bin 0 -> 108696 bytes tests/unit/test_embedding_seams.py | 38 +- tests/unit/test_logging.py | 34 +- tests/unit/test_patterns.py | 52 +- tests/unit/test_template_mining.py | 199 ++++++++ uv.lock | 72 +-- 17 files changed, 990 insertions(+), 271 deletions(-) create mode 100644 src/logreducer/template_mining.py create mode 100644 tests/integration/test_template_equivalence.py create mode 100644 tests/testdata/template_mining_golden.json.gz create mode 100644 tests/unit/test_template_mining.py diff --git a/README.md b/README.md index a824b62..902880f 100644 --- a/README.md +++ b/README.md @@ -293,8 +293,9 @@ reducer = logreducer.LogReducer(mode="hybrid", config=cfg) # ...or let logreducer read your env cascade directly: prefixed beats bare. cfg = logreducer.BigDialConfig.from_env("MYAPP", "LOGREDUCER") # MYAPP_MAX_MEMORY_GB wins -# 2. Logging: your app owns the loguru sinks; logreducer registers nothing and -# its records flow through YOUR handlers, formatted by YOUR standard. +# 2. Logging: your app owns the stdlib logging handlers; logreducer attaches +# nothing and its records flow through YOUR handlers, at YOUR level, +# formatted by YOUR standard. logreducer.setup_logging(enable=True, own_sinks=False) ``` @@ -324,7 +325,7 @@ Logging is off by default. Enable it and pick a format via env vars: LOG_LEVEL=DEBUG LOG_FORMAT=json logreducer app.log --log ``` -`LOG_FORMAT=json` emits one JSON object per line for log aggregators; the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above. +Logging is the standard library `logging` module, under the `logreducer` logger. `LOG_FORMAT=json` emits one JSON object per line for log aggregators (keys `timestamp`, `level`, `target`, `function`, `line_number`, `message`, `fields`); the default is human-readable (coloured in a terminal, plain in CI/containers). Embedding? See `own_sinks=False` above. ## Development @@ -342,6 +343,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the toolchain, test layout (including Copyright 2026 HYPERI PTY LIMITED. +Credits: template mining (`src/logreducer/template_mining.py`) is derived from [Drain3](https://github.com/logpai/Drain3), Copyright (c) 2020-2022 International Business Machines and the Drain3 project contributors, MIT licence. Drain3 builds on the Drain parser in LogPAI's [logparser](https://github.com/logpai/logparser). The module header carries Drain3's licence text in full. + ## Context Depth: [docs/architecture.md](docs/architecture.md). @@ -362,6 +365,7 @@ it is given and drops the rest. | `src/logreducer/sources.py`, `sinks.py` | the two structural protocols that ARE the integration surface, plus `FileSource` and `FileSink` | | `src/logreducer/sql.py`, `clickhouse.py`, `kafka.py` | the optional adapters, one extra each | | `src/logreducer/patterns.py`, `anomaly.py`, `temporal.py` | Drain3 mining and fuzzy dedup, Isolation Forest, time-window grouping | +| `src/logreducer/template_mining.py` | the Drain template miner, derived from Drain3 (credited under License) | | `src/logreducer/memory.py` | `MemoryMonitor`, `BoundedDeduplicator`, the file read strategies | | `src/logreducer/sampling.py`, `target.py` | dialect-aware SQL sampling, and the `reduce_to_target` batch loop | | `src/logreducer/config.py` | `BigDialConfig`, the level presets, the `from_env` cascade | @@ -407,7 +411,7 @@ One declared edge in `dfe-infra/suite.yaml`, and it runs the surprising way. `dfe-stack suite --consumer logreducer` returns an empty edge list: no repo in that graph is something this library depends on. The runtime dependencies -(drain3, psutil, loguru, numpy, scikit-learn, typer) are packages, declared in +(psutil, numpy, scikit-learn, typer) are packages, declared in `pyproject.toml`, which is where they belong. The reason dfe-engine gives for not declaring the dependency -- pin it "once it diff --git a/pyproject.toml b/pyproject.toml index e009003..420695d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,11 +32,9 @@ classifiers = [ ] keywords = ["log", "logging", "reduction", "analysis", "pattern-extraction", "memory-efficient", "streaming", "anomaly-detection"] dependencies = [ - "drain3>=0.9.0", - "psutil>=7.2.0", # memory monitoring (MemoryMonitor) - "loguru>=0.7.3", - "numpy>=2.0.0", - "scikit-learn>=1.5.0", # imports as sklearn; >=1.5 for numpy 2 support + "psutil>=7.2.2", # memory monitoring (MemoryMonitor) + "numpy>=2.5.3", + "scikit-learn>=1.9.1", # imports as sklearn "typer>=0.27.3", # CLI ] @@ -51,13 +49,13 @@ enhanced = [ # wants a ready-made source/sink. Each needs its own running service, so they # are covered by docker-gated integration tests. sql = [ - "sqlalchemy>=2.0.30", # cross-DB server-side-cursor streaming (SQLSource) + "sqlalchemy>=2.1.3", # cross-DB server-side-cursor streaming (SQLSource) ] clickhouse = [ - "clickhouse-connect>=1.0", # native block streaming (ClickHouseSource); 1.0 was breaking + "clickhouse-connect>=1.9.0", # native block streaming (ClickHouseSource) ] kafka = [ - "confluent-kafka>=2.14.0", # librdkafka client (KafkaSource/Sink) + "confluent-kafka>=2.15.1", # librdkafka client (KafkaSource/Sink) ] [project.urls] Homepage = "https://github.com/hyperi-io/logreducer" @@ -142,7 +140,7 @@ line-ending = "auto" # MyPy: KEPT STRICT (logreducer is small and already clean-typed). # ignore_missing_imports only silences third-party libs without stubs -# (drain3, datasketch, ...); our own code stays fully typed via +# (datasketch, ...); our own code stays fully typed via # disallow_untyped_defs below. [tool.mypy] python_version = "3.12" # Held below requires-python with ruff; see target-version @@ -265,9 +263,9 @@ dev = [ "vermin>=1.6.0", # Python version-compat detection # Adapter + integration test deps (TEST ONLY - never in the published # wheel; these mirror the optional extras so the adapters can be exercised). - "sqlalchemy>=2.0.30", # SQLSource against SQLite (real, no server) - "clickhouse-connect>=1.0", # ClickHouseSource (local cluster or docker) - "confluent-kafka>=2.14.0", # KafkaSource/KafkaSink + "sqlalchemy>=2.1.3", # SQLSource against SQLite (real, no server) + "clickhouse-connect>=1.9.0", # ClickHouseSource (local cluster or docker) + "confluent-kafka>=2.15.1", # KafkaSource/KafkaSink "testcontainers[clickhouse,kafka,postgres,mysql]>=4.15.0", # docker fallback; modules under testcontainers.community "psycopg[binary]>=3.2", # PostgreSQL driver (real TABLESAMPLE/setseed sampling tests) "pymysql>=1.1.0", # MySQL driver (real RAND(seed) sampling tests) diff --git a/src/logreducer/anomaly.py b/src/logreducer/anomaly.py index 5683393..2ec83b2 100644 --- a/src/logreducer/anomaly.py +++ b/src/logreducer/anomaly.py @@ -1,6 +1,8 @@ """Anomaly detection utilities.""" -from loguru import logger +import logging + +logger = logging.getLogger(__name__) # Try optional imports try: diff --git a/src/logreducer/config.py b/src/logreducer/config.py index e61469e..f5c7ddd 100644 --- a/src/logreducer/config.py +++ b/src/logreducer/config.py @@ -1,5 +1,6 @@ """Configuration and tuning parameters for LogReducer.""" +import logging import os import types import typing @@ -7,7 +8,8 @@ from enum import Enum import psutil -from loguru import logger + +logger = logging.getLogger(__name__) class ProcessingLevel(Enum): diff --git a/src/logreducer/logging_config.py b/src/logreducer/logging_config.py index 794e0c3..28f3b7e 100644 --- a/src/logreducer/logging_config.py +++ b/src/logreducer/logging_config.py @@ -1,4 +1,4 @@ -"""Logging configuration for LogReducer. +"""Logging configuration for LogReducer, on the standard library ``logging``. Output follows one strict standard so logreducer's logs sit uniformly beside a host application's logs in a shared deployment: RFC 3339 timestamps, a @@ -6,8 +6,8 @@ solarized colours in an interactive terminal, a plain prefix in CI, and one JSON object per line when ``LOG_FORMAT=json``. -logreducer is a library, not a service, so it only ever manages its OWN loguru -handlers (filtered to ``logreducer`` records) and never removes a host +logreducer is a library, not a service, so it only ever attaches handlers to its +OWN ``logreducer`` logger and never touches the root logger or a host application's handlers - embedding logreducer inside another application must not disturb that application's logging. @@ -17,17 +17,15 @@ LOG_OUTPUT=stdout|stderr """ -import contextlib +import json +import logging +import logging.handlers import os import sys +from datetime import UTC, datetime from pathlib import Path -from typing import TYPE_CHECKING -from loguru import logger - -if TYPE_CHECKING: - # Read by get_logger's quoted return annotation, which vulture does not parse. - from loguru import Logger # noqa: V104 +PACKAGE_LOGGER_NAME = "logreducer" # Solarized palette (https://ethanschoonover.com/solarized/) for the console # sink - readable on both light and dark terminal backgrounds. @@ -41,9 +39,29 @@ "red": "#dc322f", } -# Handler ids this module has registered, so re-configuring only tears down our -# own sinks - never the host application's. -_HANDLER_IDS: list[int] = [] +_LEVEL_COLOURS = { + logging.DEBUG: SOLARIZED["base01"], + logging.INFO: SOLARIZED["blue"], + logging.WARNING: SOLARIZED["yellow"], + logging.ERROR: SOLARIZED["orange"], + logging.CRITICAL: SOLARIZED["red"], +} + +# Above CRITICAL, so a disabled logreducer emits nothing, not even through a host's handlers. +_DISABLED_LEVEL = logging.CRITICAL + 1 + +# Rotate the file sink at 10 MB and keep seven rotated files. +_FILE_MAX_BYTES = 10 * 1024 * 1024 +_FILE_BACKUPS = 7 + +# Every attribute a bare LogRecord carries; anything else came from the caller's ``extra=``. +_STANDARD_RECORD_ATTRS = frozenset(logging.LogRecord("", 0, "", 0, "", (), None).__dict__) | {"message", "asctime"} + +# Handlers this module has attached, so re-configuring only tears down our own +# sinks - never the host application's. +_HANDLERS: list[logging.Handler] = [] + +logger = logging.getLogger(__name__) def _is_ci() -> bool: @@ -67,34 +85,74 @@ def _is_interactive() -> bool: return "UTF-8" in locale or "UTF8" in locale -def _console_format(colorize: bool, ci: bool) -> str: - """Console format string - solarized colours, or a plain CI prefix.""" - if ci or not colorize: - return "[{level: <8}] {name}:{function}:{line} - {message}" - return ( - f"{{time:YYYY-MM-DDTHH:mm:ss.SSSZZ}} | " - f"{{level: <8}} | " - f"{{name}}:" - f"{{function}}:" - f"{{line}} - " - f"{{message}}" - ) +def _paint(text: str, hex_colour: str) -> str: + """Wrap ``text`` in a 24-bit ANSI foreground colour.""" + red, green, blue = (int(hex_colour[i : i + 2], 16) for i in (1, 3, 5)) + return f"\x1b[38;2;{red};{green};{blue}m{text}\x1b[0m" + +def _local_rfc3339(created: float) -> str: + """RFC 3339 local time with milliseconds and the UTC offset.""" + return datetime.fromtimestamp(created).astimezone().isoformat(timespec="milliseconds") -def _file_format() -> str: - """File format string - ASCII only, RFC 3339 timestamp with bracketed level.""" - return "{time:YYYY-MM-DDTHH:mm:ss.SSSZZ} [{level: <8}] {name}:{function}:{line} - {message}" +class _LineFormatter(logging.Formatter): + """Render ``[time] LEVEL name:function:line - message`` on one line (plus any traceback).""" -def _apply_solarized_levels() -> None: - """Colour the loguru levels with the solarized scheme.""" - logger.level("TRACE", color=f"") - logger.level("DEBUG", color=f"") - logger.level("INFO", color=f"") - logger.level("SUCCESS", color=f"") - logger.level("WARNING", color=f"") - logger.level("ERROR", color=f"") - logger.level("CRITICAL", color=f"") + def __init__(self, *, with_time: bool, colour: bool, ascii_only: bool) -> None: + super().__init__() + self._with_time = with_time + self._colour = colour + self._ascii_only = ascii_only + + def format(self, record: logging.LogRecord) -> str: + """Render one record.""" + level = f"{record.levelname:<8}" + where = f"{record.name}:{record.funcName}:{record.lineno}" + message = record.getMessage() + if record.exc_info: + message = f"{message}\n{self.formatException(record.exc_info)}" + + if self._colour: + level_colour = _LEVEL_COLOURS.get(record.levelno, SOLARIZED["blue"]) + line = ( + f"{_paint(_local_rfc3339(record.created), SOLARIZED['green'])} | " + f"{_paint(level, level_colour)} | {_paint(where, SOLARIZED['cyan'])} - {_paint(message, level_colour)}" + ) + elif self._with_time: + line = f"{_local_rfc3339(record.created)} [{level}] {where} - {message}" + else: + line = f"[{level}] {where} - {message}" + + if self._ascii_only: + return line.encode("ascii", "backslashreplace").decode("ascii") + return line + + +class _JsonFormatter(logging.Formatter): + """Render one flat JSON object per record, keyed like scalo's JSON log lines.""" + + def format(self, record: logging.LogRecord) -> str: + """Render one record.""" + timestamp = datetime.fromtimestamp(record.created, UTC).isoformat(timespec="microseconds") + entry: dict[str, object] = { + "timestamp": timestamp.replace("+00:00", "Z"), + "level": record.levelname, + "target": record.name, + "function": record.funcName, + "line_number": record.lineno, + "message": record.getMessage(), + "fields": {key: value for key, value in record.__dict__.items() if key not in _STANDARD_RECORD_ATTRS}, + } + if record.exc_info: + entry["exception"] = self.formatException(record.exc_info) + return json.dumps(entry, default=str, ensure_ascii=True) + + +def _attach(package_logger: logging.Logger, handler: logging.Handler, formatter: logging.Formatter) -> None: + handler.setFormatter(formatter) + package_logger.addHandler(handler) + _HANDLERS.append(handler) def setup_logging( @@ -107,88 +165,73 @@ def setup_logging( ) -> None: """Configure logreducer's own logging sinks. + Records always propagate to the host application's handlers as well, so a + host that has configured the root logger sees logreducer's records there. + Args: enable: Whether logreducer emits logs at all (default False). - log_file: Optional path for an ASCII, rotating file sink. + log_file: Optional path for an ASCII file sink that rotates at 10 MB + and keeps seven rotated files. log_level: Threshold level (overridden by ``LOG_LEVEL``). log_format: ``text`` (human) or ``json`` (overridden by ``LOG_FORMAT``). console: Also log to the console (stderr, or ``LOG_OUTPUT=stdout``). - own_sinks: When False, register NO handlers - just enable the package - and let logreducer's records flow through the HOST application's - loguru handlers. This is the embedding seam: a host app that owns + own_sinks: When False, attach NO handlers and leave the threshold to the + host - logreducer's records flow to the HOST application's handlers + at the host's level. This is the embedding seam: a host app that owns logging calls ``setup_logging(enable=True, own_sinks=False)`` and logreducer's output lands in the host's sinks, formatted by the host's standard. + + Raises: + ValueError: If the resolved level is not a ``logging`` level name. """ # Env beats caller, so a deployment can retune logging without code changes. log_level = os.environ.get("LOG_LEVEL", log_level).upper() fmt_selector = (os.environ.get("LOG_FORMAT") or log_format or "text").strip().lower() serialize = fmt_selector == "json" - # Tear down ONLY our previously registered handlers. - global _HANDLER_IDS - for handler_id in _HANDLER_IDS: - try: - logger.remove(handler_id) - except ValueError: - pass - _HANDLER_IDS = [] + # Tear down ONLY our previously attached handlers. + package_logger = logging.getLogger(PACKAGE_LOGGER_NAME) + for handler in _HANDLERS: + package_logger.removeHandler(handler) + handler.close() + _HANDLERS.clear() if not enable: - logger.disable("logreducer") + package_logger.setLevel(_DISABLED_LEVEL) return if not own_sinks: - # Host-owned logging: enable the package, register nothing. - logger.enable("logreducer") + package_logger.setLevel(logging.NOTSET) return + package_logger.setLevel(log_level) ci = _is_ci() - stream = sys.stdout if os.environ.get("LOG_OUTPUT") == "stdout" else sys.stderr if console: - # CLI/app mode owns the console: drop loguru's default stderr handler - # (id 0) so our formatted line is not printed twice. Only the pristine - # default is removed - a host app's own handlers (ids > 0) are untouched - # (embedding logreducer only ever goes through the console=False path). - with contextlib.suppress(ValueError): - logger.remove(0) - colorize = not serialize and not ci and _is_interactive() - if colorize: - _apply_solarized_levels() - _HANDLER_IDS.append( - logger.add( - stream, - level=log_level, - format=_console_format(colorize, ci), - colorize=colorize, - serialize=serialize, - filter="logreducer", - ) + stream = sys.stdout if os.environ.get("LOG_OUTPUT") == "stdout" else sys.stderr + colour = not serialize and not ci and _is_interactive() + console_formatter = ( + _JsonFormatter() if serialize else _LineFormatter(with_time=colour, colour=colour, ascii_only=False) ) + _attach(package_logger, logging.StreamHandler(stream), console_formatter) if log_file: try: Path(log_file).parent.mkdir(parents=True, exist_ok=True) - _HANDLER_IDS.append( - logger.add( - log_file, - level=log_level, - format=_file_format(), - rotation="10 MB", - retention="7 days", - encoding="utf-8", - serialize=serialize, - filter="logreducer", - ) + file_handler = logging.handlers.RotatingFileHandler( + log_file, maxBytes=_FILE_MAX_BYTES, backupCount=_FILE_BACKUPS, encoding="utf-8" ) except OSError as exc: # A bad log path should never take the process down. - logger.warning(f"Could not create log file {log_file}: {exc}") - - logger.enable("logreducer") + logger.warning("Could not create log file %s: %s", log_file, exc) + else: + file_formatter = ( + _JsonFormatter() if serialize else _LineFormatter(with_time=True, colour=False, ascii_only=True) + ) + _attach(package_logger, file_handler, file_formatter) -def get_logger(name: str = "logreducer") -> "Logger": - """Return a logger bound for the given logreducer submodule name.""" - return logger.bind(name=name) +def get_logger(name: str = PACKAGE_LOGGER_NAME) -> logging.Logger: + """Return the logger for a logreducer module name (``logreducer.``).""" + return logging.getLogger(name) diff --git a/src/logreducer/patterns.py b/src/logreducer/patterns.py index cfa89a2..fa95ab3 100644 --- a/src/logreducer/patterns.py +++ b/src/logreducer/patterns.py @@ -1,17 +1,17 @@ """Pattern extraction (Drain3 template mining) and fuzzy deduplication.""" +import logging from collections.abc import Iterable, Iterator from dataclasses import dataclass, field from typing import TYPE_CHECKING -from drain3 import TemplateMiner -from drain3.masking import MaskingInstruction -from drain3.template_miner_config import TemplateMinerConfig -from loguru import logger +from .template_mining import MaskingInstruction, TemplateMiner, TemplateMinerConfig if TYPE_CHECKING: from .config import BigDialConfig +logger = logging.getLogger(__name__) + # datasketch powers fuzzy dedup and ships in the optional `enhanced` extra; # without it FuzzyDeduplicator degrades to a pass-through (with a warning). try: @@ -34,10 +34,10 @@ class LogPattern: metadata: dict = field(default_factory=dict) -def _typed_masking_instructions() -> list[MaskingInstruction]: +def _typed_masking_instructions() -> tuple[MaskingInstruction, ...]: """Build the curated masking set for typed template slots. - Drain3 applies these to each line before clustering, so masked values + The miner applies these to each line before clustering, so masked values become literal typed tokens (````, ````, ...) rather than collapsing to the bare ``<*>`` wildcard. Instructions run sequentially over already-masked text, so the specific shapes (IP, UUID, MAC, IPv6) @@ -70,52 +70,47 @@ def _typed_masking_instructions() -> list[MaskingInstruction]: # Integer (drain3's documented NUM example). (r"[-+]?\d+", "NUM"), ] - return [MaskingInstruction(f"{start}(?:{pattern}){end}", name) for pattern, name in shapes] + return tuple(MaskingInstruction(f"{start}(?:{pattern}){end}", name) for pattern, name in shapes) class PatternExtractor: - """Extract patterns using Drain3's online template miner.""" + """Extract patterns using the online template miner (see template_mining).""" def __init__(self, config: "BigDialConfig") -> None: self.config = config self.setup_drain3() def setup_drain3(self) -> None: - """Configure Drain3.""" - drain_config = TemplateMinerConfig() - drain_config.profiling_enabled = False - drain_config.drain_sim_th = self.config.drain_similarity - drain_config.drain_depth = 4 - drain_config.snapshot_interval_minutes = 0 # Disable snapshots - drain_config.snapshot_compress_state = False - # Bound the template store when configured: Drain3 LRU-evicts beyond - # drain_max_clusters, keeping memory flat on high-cardinality logs. - drain_config.drain_max_clusters = self.config.max_clusters + """Configure the template miner.""" # Opt-in typed masking: pre-cluster masking of well-known value shapes # so templates carry typed slots (, , ...) instead of <*>. - if self.config.typed_masking: - drain_config.masking_instructions = _typed_masking_instructions() - self.miner = TemplateMiner(config=drain_config) + masking = _typed_masking_instructions() if self.config.typed_masking else () + drain_config = TemplateMinerConfig( + drain_sim_th=self.config.drain_similarity, + drain_depth=4, + # Bound the template store when configured: the miner LRU-evicts beyond + # drain_max_clusters, keeping memory flat on high-cardinality logs. + drain_max_clusters=self.config.max_clusters, + masking_instructions=masking, + ) + self.miner = TemplateMiner(drain_config) def extract_patterns(self, lines: Iterable[str]) -> list[LogPattern]: """Extract patterns from lines (accepts a stream; only clusters are held). - Feeds each line into Drain3's online miner, so the caller can pass a + Feeds each line into the online miner, so the caller can pass a generator - no need to materialise the whole line set. Memory is bounded by the number of clusters (see ``max_clusters``), not the line count. """ - pattern_map = {} + pattern_map: dict[int, LogPattern] = {} for line in lines: - result = self.miner.add_log_message(line) - cluster_id = result["cluster_id"] - - if cluster_id not in pattern_map: - cluster = self.miner.drain.id_to_cluster[cluster_id] + cluster = self.miner.add_log_message(line) + pattern = pattern_map.get(cluster.cluster_id) + if pattern is None: pattern = LogPattern(template=cluster.get_template(), count=0) - pattern_map[cluster_id] = pattern + pattern_map[cluster.cluster_id] = pattern - pattern = pattern_map[cluster_id] pattern.count += 1 if len(pattern.examples) < self.config.examples_per_pattern: @@ -123,11 +118,10 @@ def extract_patterns(self, lines: Iterable[str]) -> list[LogPattern]: # Later lines widen a cluster's slots to <*>, so the stored template is # re-read once the feed ends; an LRU-evicted cluster keeps its snapshot. - clusters = self.miner.drain.id_to_cluster for cluster_id, pattern in pattern_map.items(): - cluster = clusters.get(cluster_id) - if cluster is not None: - pattern.template = cluster.get_template() + live_cluster = self.miner.get_cluster(cluster_id) + if live_cluster is not None: + pattern.template = live_cluster.get_template() patterns = list(pattern_map.values()) diff --git a/src/logreducer/sql.py b/src/logreducer/sql.py index 474f3aa..2b8e232 100644 --- a/src/logreducer/sql.py +++ b/src/logreducer/sql.py @@ -143,7 +143,7 @@ def from_table( from .sampling import build_table_sample_query owns = isinstance(connectable, str) - engine = create_engine(connectable) if owns else connectable + engine = create_engine(connectable) if isinstance(connectable, str) else connectable preparer = engine.dialect.identifier_preparer query = build_table_sample_query( engine.dialect.name, diff --git a/src/logreducer/template_mining.py b/src/logreducer/template_mining.py new file mode 100644 index 0000000..065d9d9 --- /dev/null +++ b/src/logreducer/template_mining.py @@ -0,0 +1,452 @@ +# Upstream: Drain3, https://github.com/logpai/Drain3 +# +# SPDX-License-Identifier: MIT +# This file implements the Drain algorithm for log parsing. +# Based on https://github.com/logpai/logparser/blob/master/logparser/Drain/Drain.py by LogPAI team +# +# MIT License +# +# Copyright (c) 2020-2022 International Business Machines +# and the Drain3 project contributors. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Project: logreducer +# File: src/logreducer/template_mining.py +# Purpose: Online log template mining (the Drain algorithm) with regex pre-masking +# Language: Python +# +# License: Apache-2.0 +# Copyright: (c) 2026 HYPERI PTY LIMITED + +"""Online log template mining with the Drain algorithm. + +Derived from Drain3 at commit 0fb6d6e1828838fa05c8f6fa5cda878e02ea79c7. It keeps +only the streaming path logreducer drives: mask a line, route it through a +fixed-depth prefix tree, and merge it into the most similar cluster of the same +token count, widening differing tokens to ``<*>``. NOTICE lists what changed +from upstream. +""" + +import re +from collections import OrderedDict +from collections.abc import Iterable, Sequence +from dataclasses import dataclass + +# Wildcard a widened template slot carries; masks render as in the same shape. +_PARAM_STR = "<*>" + + +class LogCluster: + """One mined template and the number of lines merged into it. + + Attributes: + log_template_tokens: The template, one token per whitespace-split word. + cluster_id: Sequential id, never reused within a miner. + size: Lines merged into this cluster. + """ + + __slots__ = ("cluster_id", "log_template_tokens", "size") + + def __init__(self, log_template_tokens: Iterable[str], cluster_id: int) -> None: + self.log_template_tokens = tuple(log_template_tokens) + self.cluster_id = cluster_id + self.size = 1 + + def get_template(self) -> str: + """Return the template as one space-joined string.""" + return " ".join(self.log_template_tokens) + + +class ClusterStore: + """Cluster id to cluster map, optionally bounded by least-recently-used eviction. + + Storing and ``touch`` mark a cluster as used; ``get`` and ``in`` only look, so + scoring candidate clusters never changes which one is evicted next. + """ + + __slots__ = ("_clusters", "_maxsize") + + def __init__(self, maxsize: int | None = None) -> None: + """Create an empty store. + + Args: + maxsize: Clusters kept before the least recently used is evicted. + ``None`` keeps every cluster. + + Raises: + ValueError: If ``maxsize`` is below 1. + """ + if maxsize is not None and maxsize < 1: + raise ValueError(f"maxsize must be at least 1, not {maxsize}") + self._maxsize = maxsize + self._clusters: OrderedDict[int, LogCluster] = OrderedDict() + + def __setitem__(self, cluster_id: int, cluster: LogCluster) -> None: + full = self._maxsize is not None and len(self._clusters) >= self._maxsize + if full and cluster_id not in self._clusters: + self._clusters.popitem(last=False) + self._clusters[cluster_id] = cluster + self._clusters.move_to_end(cluster_id) + + def __contains__(self, cluster_id: object) -> bool: + return cluster_id in self._clusters + + def __len__(self) -> int: + return len(self._clusters) + + def get(self, cluster_id: int) -> LogCluster | None: + """Return the cluster without marking it used, or ``None`` if absent or evicted.""" + return self._clusters.get(cluster_id) + + def touch(self, cluster_id: int) -> None: + """Mark a stored cluster as the most recently used.""" + self._clusters.move_to_end(cluster_id) + + def values(self) -> list[LogCluster]: + """Return the stored clusters, least recently used first.""" + return list(self._clusters.values()) + + +class Node: + """A prefix-tree node: child nodes keyed by token, and the clusters stored here.""" + + __slots__ = ("cluster_ids", "key_to_child_node") + + def __init__(self) -> None: + self.key_to_child_node: dict[str, Node] = {} + self.cluster_ids: list[int] = [] + + +def _has_numbers(token: str) -> bool: + return any(char.isdigit() for char in token) + + +class Drain: + """The Drain online clustering algorithm over a fixed-depth prefix tree. + + The first tree level splits lines by token count, the next levels by leading + tokens (a token containing a digit routes to the ``<*>`` child), and each leaf + holds the clusters a line is scored against. + + Attributes: + sim_th: Minimum share of matching tokens to join an existing cluster. + max_children: Children a tree node holds before new tokens share ``<*>``. + root_node: Root of the prefix tree. + id_to_cluster: Every live cluster, keyed by id. + clusters_counter: The id given to the newest cluster. + """ + + def __init__( + self, + depth: int = 4, + sim_th: float = 0.4, + max_children: int = 100, + max_clusters: int | None = None, + ) -> None: + """Create an empty miner. + + Args: + depth: Tree depth, counting the root and the token-count level. + Depth 4 routes on the first token only. + sim_th: Minimum share of matching tokens to join an existing cluster. + max_children: Children a tree node holds before new tokens share ``<*>``. + max_clusters: Clusters kept before the least recently used is evicted. + ``None`` keeps every cluster. + + Raises: + ValueError: If ``depth`` is below 3 or ``max_clusters`` is below 1. + """ + if depth < 3: + raise ValueError(f"depth must be at least 3, not {depth}") + self.max_node_depth = depth - 2 + self.sim_th = sim_th + self.max_children = max_children + self.root_node = Node() + self.id_to_cluster = ClusterStore(max_clusters) + self.clusters_counter = 0 + + @property + def clusters(self) -> list[LogCluster]: + """Return every live cluster.""" + return self.id_to_cluster.values() + + def add_log_message(self, content: str) -> LogCluster: + """Merge one (already masked) line into its cluster, creating one if none matches. + + Args: + content: The line to cluster. + + Returns: + The cluster the line joined. Its template reflects this line. + """ + content_tokens = content.split() + match_cluster = self.tree_search(content_tokens) + + if match_cluster is None: + self.clusters_counter += 1 + match_cluster = LogCluster(content_tokens, self.clusters_counter) + self.id_to_cluster[match_cluster.cluster_id] = match_cluster + self.add_seq_to_prefix_tree(match_cluster) + return match_cluster + + match_cluster.log_template_tokens = self.create_template(content_tokens, match_cluster.log_template_tokens) + match_cluster.size += 1 + self.id_to_cluster.touch(match_cluster.cluster_id) + return match_cluster + + def tree_search(self, tokens: Sequence[str]) -> LogCluster | None: + """Return the best live cluster for ``tokens`` at or above ``sim_th``, or ``None``. + + Args: + tokens: The line, split on whitespace. + + Returns: + The matching cluster, or ``None`` when the line needs a new one. + """ + token_count = len(tokens) + cur_node = self.root_node.key_to_child_node.get(str(token_count)) + if cur_node is None: + return None + + # Empty lines share the single cluster stored at their token-count node. + if token_count == 0: + return self.id_to_cluster.get(cur_node.cluster_ids[0]) + + cur_node_depth = 1 + for token in tokens: + if cur_node_depth >= self.max_node_depth or cur_node_depth == token_count: + break + child = cur_node.key_to_child_node.get(token) + if child is None: + child = cur_node.key_to_child_node.get(_PARAM_STR) + if child is None: + return None + cur_node = child + cur_node_depth += 1 + + return self.fast_match(cur_node.cluster_ids, tokens) + + def add_seq_to_prefix_tree(self, cluster: LogCluster) -> None: + """Route a new cluster down the tree and store its id at the leaf it reaches. + + Args: + cluster: The cluster just created. + """ + token_count = len(cluster.log_template_tokens) + token_count_str = str(token_count) + if token_count_str not in self.root_node.key_to_child_node: + self.root_node.key_to_child_node[token_count_str] = Node() + cur_node = self.root_node.key_to_child_node[token_count_str] + + if token_count == 0: + cur_node.cluster_ids = [cluster.cluster_id] + return + + current_depth = 1 + for token in cluster.log_template_tokens: + if current_depth >= self.max_node_depth or current_depth >= token_count: + # Drop ids the store has evicted before adding the new one. + live_ids = [cluster_id for cluster_id in cur_node.cluster_ids if cluster_id in self.id_to_cluster] + live_ids.append(cluster.cluster_id) + cur_node.cluster_ids = live_ids + break + + children = cur_node.key_to_child_node + if token in children: + cur_node = children[token] + elif _has_numbers(token): + if _PARAM_STR not in children: + children[_PARAM_STR] = Node() + cur_node = children[_PARAM_STR] + elif _PARAM_STR in children: + if len(children) < self.max_children: + children[token] = Node() + cur_node = children[token] + else: + cur_node = children[_PARAM_STR] + elif len(children) + 1 < self.max_children: + children[token] = Node() + cur_node = children[token] + elif len(children) + 1 == self.max_children: + children[_PARAM_STR] = Node() + cur_node = children[_PARAM_STR] + else: + cur_node = children[_PARAM_STR] + + current_depth += 1 + + def fast_match(self, cluster_ids: Sequence[int], tokens: Sequence[str]) -> LogCluster | None: + """Return the most similar live cluster at or above ``sim_th``, or ``None``. + + Ties on similarity go to the cluster with more ``<*>`` slots, then to the + earlier candidate. + + Args: + cluster_ids: Candidate clusters stored at the leaf the line reached. + tokens: The line, split on whitespace. + + Returns: + The best candidate, or ``None`` if none reaches ``sim_th``. + """ + max_sim = -1.0 + max_param_count = -1 + max_cluster = None + + for cluster_id in cluster_ids: + cluster = self.id_to_cluster.get(cluster_id) + if cluster is None: + continue + cur_sim, param_count = self.get_seq_distance(cluster.log_template_tokens, tokens) + if cur_sim > max_sim or (cur_sim == max_sim and param_count > max_param_count): + max_sim = cur_sim + max_param_count = param_count + max_cluster = cluster + + if max_sim >= self.sim_th: + return max_cluster + return None + + def get_seq_distance(self, template: Sequence[str], tokens: Sequence[str]) -> tuple[float, int]: + """Score a line against a template of the same length. + + ``<*>`` slots count towards neither similarity nor mismatch. + + Args: + template: The cluster template tokens. + tokens: The line tokens. + + Returns: + The share of template tokens the line matches exactly, and the number + of ``<*>`` slots in the template. + """ + if not template: + return 1.0, 0 + + sim_tokens = 0 + param_count = 0 + for template_token, token in zip(template, tokens, strict=True): + if template_token == _PARAM_STR: + param_count += 1 + elif template_token == token: + sim_tokens += 1 + + return sim_tokens / len(template), param_count + + def create_template(self, tokens: Sequence[str], template: Sequence[str]) -> tuple[str, ...]: + """Widen every template token the line does not match to ``<*>``. + + Args: + tokens: The line tokens. + template: The current template tokens. + + Returns: + The merged template. + """ + return tuple( + template_token if token == template_token else _PARAM_STR + for token, template_token in zip(tokens, template, strict=True) + ) + + +class MaskingInstruction: + """Replace every regex match in a line with ```` before clustering. + + Attributes: + regex: The compiled pattern. + mask_with: Name the match renders as, inside angle brackets. + """ + + __slots__ = ("mask_with", "regex") + + def __init__(self, pattern: str, mask_with: str) -> None: + """Compile a masking rule. + + Args: + pattern: Regular expression for the values to mask. + mask_with: Name the match renders as, inside angle brackets. + """ + self.regex = re.compile(pattern) + self.mask_with = mask_with + + def mask(self, content: str) -> str: + """Return ``content`` with every match replaced by ````.""" + return self.regex.sub(f"<{self.mask_with}>", content) + + +@dataclass(frozen=True, slots=True) +class TemplateMinerConfig: + """Tuning for a :class:`TemplateMiner`. + + Attributes: + drain_sim_th: Minimum share of matching tokens to join an existing cluster. + drain_depth: Prefix-tree depth, counting the root and the token-count level. + drain_max_children: Children a tree node holds before new tokens share ``<*>``. + drain_max_clusters: Clusters kept before the least recently used is evicted. + ``None`` keeps every cluster. + masking_instructions: Masks applied to each line in order, before clustering. + """ + + drain_sim_th: float = 0.4 + drain_depth: int = 4 + drain_max_children: int = 100 + drain_max_clusters: int | None = None + masking_instructions: tuple[MaskingInstruction, ...] = () + + +class TemplateMiner: + """Mask each line, then cluster it with :class:`Drain`. + + Attributes: + config: The tuning this miner was built with. + drain: The clustering state. + """ + + def __init__(self, config: TemplateMinerConfig | None = None) -> None: + """Create an empty miner. + + Args: + config: Tuning; ``None`` uses the :class:`TemplateMinerConfig` defaults. + """ + self.config = config if config is not None else TemplateMinerConfig() + self.drain = Drain( + depth=self.config.drain_depth, + sim_th=self.config.drain_sim_th, + max_children=self.config.drain_max_children, + max_clusters=self.config.drain_max_clusters, + ) + + def add_log_message(self, log_message: str) -> LogCluster: + """Mask one line and merge it into its cluster. + + Args: + log_message: The raw line. + + Returns: + The cluster the line joined. Its template keeps widening as later + lines join, so read it again once the feed ends. + """ + masked = log_message + for instruction in self.config.masking_instructions: + masked = instruction.mask(masked) + return self.drain.add_log_message(masked) + + def get_cluster(self, cluster_id: int) -> LogCluster | None: + """Return a live cluster without marking it used, or ``None`` once evicted.""" + return self.drain.id_to_cluster.get(cluster_id) diff --git a/src/logreducer/temporal.py b/src/logreducer/temporal.py index 17d54b6..1bf1d7c 100644 --- a/src/logreducer/temporal.py +++ b/src/logreducer/temporal.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import Any -from drain3 import TemplateMiner +from .template_mining import TemplateMiner @dataclass @@ -102,21 +102,16 @@ def _extract_temporal_patterns(self, entries: list[LogEntry]) -> list[dict]: # Process each window for window_key, window_entries in windows.items(): if window_key not in self.miners_by_window: - from drain3.template_miner_config import TemplateMinerConfig - - config = TemplateMinerConfig() - config.snapshot_interval_minutes = 0 # Disable snapshots - config.snapshot_compress_state = False - self.miners_by_window[window_key] = TemplateMiner(config=config) + self.miners_by_window[window_key] = TemplateMiner() miner = self.miners_by_window[window_key] - cluster_ids = [miner.add_log_message(entry.raw_line)["cluster_id"] for entry in window_entries] + clusters = [miner.add_log_message(entry.raw_line) for entry in window_entries] # Templates are read once the window is fed, after later lines have widened their slots. - for entry, cluster_id in zip(window_entries, cluster_ids, strict=True): + for entry, cluster in zip(window_entries, clusters, strict=True): patterns.append( { - "template": miner.drain.id_to_cluster[cluster_id].get_template(), + "template": cluster.get_template(), "window": window_key, "count": 1, "example": entry.raw_line, @@ -127,21 +122,16 @@ def _extract_temporal_patterns(self, entries: list[LogEntry]) -> list[dict]: def _extract_timeless_patterns(self, entries: list[LogEntry]) -> list[dict]: """Extract patterns without timestamps.""" - from drain3.template_miner_config import TemplateMinerConfig - - config = TemplateMinerConfig() - config.snapshot_interval_minutes = 0 # Disable snapshots - config.snapshot_compress_state = False - miner = TemplateMiner(config=config) - cluster_ids = [miner.add_log_message(entry.raw_line)["cluster_id"] for entry in entries] + miner = TemplateMiner() + clusters = [miner.add_log_message(entry.raw_line) for entry in entries] # Templates are read once every entry is fed, after later lines have widened their slots. return [ { - "template": miner.drain.id_to_cluster[cluster_id].get_template(), + "template": cluster.get_template(), "example": entry.raw_line, } - for entry, cluster_id in zip(entries, cluster_ids, strict=True) + for entry, cluster in zip(entries, clusters, strict=True) ] def _analyze_distribution(self, entries: list[LogEntry]) -> dict: diff --git a/tests/integration/test_template_equivalence.py b/tests/integration/test_template_equivalence.py new file mode 100644 index 0000000..11d8e13 --- /dev/null +++ b/tests/integration/test_template_equivalence.py @@ -0,0 +1,57 @@ +"""Template mining reproduces the drain3 0.9.11 output it replaced, on real corpora. + +``tests/testdata/template_mining_golden.json.gz`` holds what drain3 0.9.11 +produced for the first ``limit`` lines of every committed corpus under each +miner configuration: the per-line cluster assignment (as a sha256 digest), the +number of live clusters, and the templates and counts ``extract_patterns`` +returned, in order. Server-free; no mocks. +""" + +import gzip +import hashlib +import json +from typing import Any + +import pytest + +from logreducer.config import BigDialConfig +from logreducer.patterns import PatternExtractor +from tests.integration import corpora + +pytestmark = pytest.mark.integration + +GOLDEN_PATH = corpora.TESTDATA_DIR / "template_mining_golden.json.gz" +GOLDEN: dict[str, Any] = json.loads(gzip.decompress(GOLDEN_PATH.read_bytes())) +CASES = [(name, label) for name in GOLDEN["results"] for label in GOLDEN["configs"]] + + +def _config(label: str) -> BigDialConfig: + # Keep every cluster, so the comparison covers the whole template set. + return BigDialConfig(min_pattern_occurrences=1, max_patterns=10**9, **GOLDEN["configs"][label]) + + +def test_golden_covers_every_corpus(): + assert sorted(GOLDEN["results"]) == sorted(corpora.dataset_names()) + + +@pytest.mark.parametrize(("name", "label"), CASES) +def test_cluster_assignment_matches_drain3(name, label): + expected = GOLDEN["results"][name][label] + lines = corpora.read_all(name, limit=GOLDEN["limit"]) + assert len(lines) == expected["lines"] + + miner = PatternExtractor(_config(label)).miner + ids = [miner.add_log_message(line).cluster_id for line in lines] + + assert hashlib.sha256(",".join(map(str, ids)).encode()).hexdigest() == expected["assignments_sha256"] + assert len(miner.drain.id_to_cluster) == expected["live_clusters"] + + +@pytest.mark.parametrize(("name", "label"), CASES) +def test_extracted_patterns_match_drain3(name, label): + expected = GOLDEN["results"][name][label] + lines = corpora.read_all(name, limit=GOLDEN["limit"]) + + patterns = PatternExtractor(_config(label)).extract_patterns(lines) + + assert [[p.template, p.count] for p in patterns] == expected["patterns"] diff --git a/tests/testdata/README.md b/tests/testdata/README.md index 18c7878..28114e8 100644 --- a/tests/testdata/README.md +++ b/tests/testdata/README.md @@ -30,6 +30,10 @@ template counts from a handful to well over a thousand. | `elastic_nginx_json` | structured | one JSON object per line | | `secrepo_auth` | security | auth.log SSH failures | +## Template-mining golden + +`template_mining_golden.json.gz` is the template-mining output drain3 0.9.11 produced on the first 3000 lines of each corpus, under five miner configurations. `tests/integration/test_template_equivalence.py` holds `logreducer.template_mining` to it. Rebuilding a corpus invalidates that corpus's entries. + ## PII cleansing `build.py` rewrites IPs (-> `10.x`), IPv6 (-> `2001:db8::`), emails diff --git a/tests/testdata/template_mining_golden.json.gz b/tests/testdata/template_mining_golden.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..56096604aadcaf45073c1d037485653a2f606676 GIT binary patch literal 108696 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

_ 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 literal 0 HcmV?d00001 diff --git a/tests/unit/test_embedding_seams.py b/tests/unit/test_embedding_seams.py index 12e10b8..f466450 100644 --- a/tests/unit/test_embedding_seams.py +++ b/tests/unit/test_embedding_seams.py @@ -75,25 +75,33 @@ def test_unset_fields_keep_defaults(self): class TestHostOwnedLogging: - def test_own_sinks_false_flows_records_to_host_handlers(self): - """The embedding seam: logreducer registers nothing; the host's own - loguru handler receives logreducer's records (which are filtered by - the caller's module name, so they must originate inside the package - - drive them via a real reduce()).""" - from loguru import logger + def test_own_sinks_false_flows_records_to_host_handlers(self, caplog, tmp_path): + """The embedding seam: logreducer attaches nothing; the host's own root + handler (caplog's, at the host's INFO level) receives logreducer's + records - driven via a real reduce() so they originate in the package.""" + import logging from logreducer import logging_config + log_file = tmp_path / "ignored-when-host-owned.log" reducer = LogReducer(level="standard", mode="pattern", enable_logging=True) - setup_logging(enable=True, own_sinks=False, console=True, log_file="ignored-when-host-owned.log") + setup_logging(enable=True, own_sinks=False, console=True, log_file=str(log_file)) try: - assert logging_config._HANDLER_IDS == [] # nothing registered by us - captured: list[str] = [] - host_id = logger.add(lambda m: captured.append(str(m)), filter="logreducer") - try: - reducer.reduce(["ERROR a", "ERROR a", "INFO b"]) - finally: - logger.remove(host_id) - assert captured, "logreducer records did not reach the host handler" + assert logging_config._HANDLERS == [] # nothing attached by us + assert logging.getLogger("logreducer").handlers == [] + caplog.set_level(logging.INFO) + reducer.reduce(["ERROR a", "ERROR a", "INFO b"]) + ours = [r for r in caplog.records if r.name.startswith("logreducer.")] + assert ours, "logreducer records did not reach the host handler" + assert not log_file.exists() finally: setup_logging(enable=False) # restore the library default + + def test_disabled_logging_reaches_no_host_handler(self, caplog): + """Disabled (the default) means silent, even with a host handler at DEBUG.""" + import logging + + caplog.set_level(logging.DEBUG) + reducer = LogReducer(level="standard", mode="pattern") + reducer.reduce(["ERROR a", "ERROR a", "INFO b"]) + assert [r for r in caplog.records if r.name.startswith("logreducer")] == [] diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 24dcf3c..02f3333 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -6,6 +6,8 @@ """ import json +import logging +from datetime import datetime import pytest @@ -38,9 +40,35 @@ def test_json_log_format(tmp_path, small_log_file, monkeypatch): reducer.process_file(str(small_log_file)) setup_logging(enable=False) - first_line = log_file.read_text().splitlines()[0] - record = json.loads(first_line) # loguru serialize=True -> one JSON object per line - assert "record" in record + lines = log_file.read_text().splitlines() + records = [json.loads(line) for line in lines] # one JSON object per line + assert records + first = records[0] + assert set(first) >= {"timestamp", "level", "target", "function", "line_number", "message", "fields"} + assert first["target"].startswith("logreducer.") + assert first["timestamp"].endswith("Z") + + +def test_text_log_file_is_ascii_with_rfc3339_time(tmp_path, small_log_file): + log_file = tmp_path / "run.log" + reducer = LogReducer(level="standard", enable_logging=True, log_file=str(log_file), log_level="INFO") + reducer.process_file(str(small_log_file)) + setup_logging(enable=False) + + first_line = log_file.read_text(encoding="utf-8").splitlines()[0] + first_line.encode("ascii") # raises if the file sink wrote a non-ASCII byte + timestamp, rest = first_line.split(" ", 1) + assert datetime.fromisoformat(timestamp).tzinfo is not None # offset-aware RFC 3339 + assert rest.startswith("[INFO ] logreducer.core:") + + +def test_file_sink_escapes_non_ascii(tmp_path): + log_file = tmp_path / "unicode.log" + setup_logging(enable=True, log_file=str(log_file), log_level="INFO") + logging.getLogger("logreducer.test").info("caf" + chr(0xE9)) + setup_logging(enable=False) + + assert log_file.read_text(encoding="ascii").rstrip().endswith("caf\\xe9") def test_disabled_logging_writes_nothing(tmp_path, small_log_file): diff --git a/tests/unit/test_patterns.py b/tests/unit/test_patterns.py index 74ffcf8..1924c8e 100644 --- a/tests/unit/test_patterns.py +++ b/tests/unit/test_patterns.py @@ -56,44 +56,36 @@ def test_initialization_with_config(self): assert extractor.config == config assert hasattr(extractor, "miner") - @patch("logreducer.patterns.TemplateMiner") - def test_extract_patterns_empty_input(self, mock_template_miner): - """Test pattern extraction with empty input""" - config = BigDialConfig() - extractor = PatternExtractor(config) - - result = extractor.extract_patterns([]) + def test_extract_patterns_empty_input(self): + """Empty input yields no patterns.""" + extractor = PatternExtractor(BigDialConfig()) - assert result == [] + assert extractor.extract_patterns([]) == [] - @patch("logreducer.patterns.TemplateMiner") - def test_extract_patterns_basic(self, mock_template_miner): - """Test basic pattern extraction""" + def test_extract_patterns_basic(self): + """Lines differing in one token merge into one widened pattern.""" config = BigDialConfig(max_patterns=10, examples_per_pattern=2) - - # Mock the miner and cluster - mock_cluster = Mock() - mock_cluster.get_template.return_value = "User <*> logged in" - - mock_miner = Mock() - mock_miner.add_log_message.return_value = {"cluster_id": 1} - mock_miner.drain.id_to_cluster = {1: mock_cluster} - mock_template_miner.return_value = mock_miner - extractor = PatternExtractor(config) - extractor.miner = mock_miner - lines = ["User alice logged in", "User bob logged in", "User charlie logged in"] result = extractor.extract_patterns(lines) - # Should have processed lines - assert mock_miner.add_log_message.call_count == len(lines) + assert len(result) == 1 + assert result[0].template == "User <*> logged in" + assert result[0].count == 3 + assert result[0].examples == lines[:2] - # Should return patterns - assert isinstance(result, list) - if len(result) > 0: - assert isinstance(result[0], LogPattern) + def test_evicted_cluster_keeps_its_first_template(self): + """A cluster LRU-evicted mid-feed keeps the template it had when first seen.""" + extractor = PatternExtractor(BigDialConfig(max_clusters=1, min_pattern_occurrences=1)) + lines = ["alpha one x", "alpha two x", "beta gamma delta epsilon"] + + result = extractor.extract_patterns(lines) + + templates = {p.template: p.count for p in result} + # "alpha" was evicted by "beta" before the feed ended, so its widened + # "alpha <*> x" is never re-read; it keeps the first line's template. + assert templates == {"alpha one x": 2, "beta gamma delta epsilon": 1} def test_filter_by_occurrence(self): """Test filtering patterns by minimum occurrence""" @@ -293,7 +285,7 @@ def test_priority_reads_the_final_template(self): def test_default_configures_no_masking_instructions(self): """Off by default: the miner carries zero masking instructions.""" extractor = PatternExtractor(BigDialConfig()) - assert list(extractor.miner.masker.masking_instructions) == [] + assert extractor.miner.config.masking_instructions == () def test_typed_masking_ip(self): """IP-bearing lines yield a typed slot, no bare wildcard.""" diff --git a/tests/unit/test_template_mining.py b/tests/unit/test_template_mining.py new file mode 100644 index 0000000..93da184 --- /dev/null +++ b/tests/unit/test_template_mining.py @@ -0,0 +1,199 @@ +"""Unit tests for the template miner (logreducer.template_mining). + +Real miner, no mocks. The cross-check against the drain3 output it replaced +lives in tests/integration/test_template_equivalence.py. +""" + +import importlib.metadata +import re +import subprocess +import sys + +import pytest + +from logreducer.template_mining import ( + ClusterStore, + Drain, + LogCluster, + MaskingInstruction, + TemplateMiner, + TemplateMinerConfig, +) + +# Packages the miner replaced; none may be imported or declared any more. +REPLACED = {"drain3", "jsonpickle", "cachetools", "loguru"} + + +class TestClustering: + def test_differing_token_widens_to_wildcard(self): + miner = TemplateMiner() + first = miner.add_log_message("user alice logged in") + second = miner.add_log_message("user bob logged in") + + assert first is second + assert second.get_template() == "user <*> logged in" + assert second.size == 2 + + def test_below_similarity_threshold_starts_a_new_cluster(self): + # One of four tokens matches: 0.25 is below the default 0.4. + miner = TemplateMiner() + first = miner.add_log_message("disk full on sda") + second = miner.add_log_message("disk ok for now") + + assert first.cluster_id != second.cluster_id + + def test_lower_threshold_merges_the_same_lines(self): + miner = TemplateMiner(TemplateMinerConfig(drain_sim_th=0.2)) + first = miner.add_log_message("disk full on sda") + second = miner.add_log_message("disk ok for now") + + assert first is second + assert second.get_template() == "disk <*> <*> <*>" + + def test_token_count_partitions_clusters(self): + miner = TemplateMiner() + short = miner.add_log_message("job done") + long = miner.add_log_message("job done quickly") + + assert short.cluster_id != long.cluster_id + + def test_empty_and_blank_lines_share_one_empty_cluster(self): + miner = TemplateMiner() + empty = miner.add_log_message("") + blank = miner.add_log_message(" \t ") + + assert empty is blank + assert empty.get_template() == "" + assert empty.size == 2 + + def test_whitespace_runs_split_like_single_spaces(self): + miner = TemplateMiner() + first = miner.add_log_message(" a b c ") + second = miner.add_log_message("a b c") + + assert first is second + assert second.get_template() == "a b c" + + def test_cluster_ids_are_sequential(self): + miner = TemplateMiner() + ids = [miner.add_log_message(line).cluster_id for line in ("a", "b c", "d e f")] + + assert ids == [1, 2, 3] + + def test_full_node_routes_new_first_tokens_to_the_wildcard_child(self): + # max_children=3: alpha and beta get their own nodes, the third slot + # becomes <*>, and gamma and delta both land under it and merge. + miner = TemplateMiner(TemplateMinerConfig(drain_max_children=3)) + clusters = [miner.add_log_message(f"{word} x y") for word in ("alpha", "beta", "gamma", "delta")] + + assert len({c.cluster_id for c in clusters}) == 3 + assert clusters[2] is clusters[3] + assert clusters[3].get_template() == "<*> x y" + + def test_numeric_first_token_routes_to_the_wildcard_child(self): + miner = TemplateMiner() + miner.add_log_message("42 requests served") + + first_layer = miner.drain.root_node.key_to_child_node["3"] + assert list(first_layer.key_to_child_node) == ["<*>"] + + def test_depth_below_three_is_rejected(self): + with pytest.raises(ValueError, match="depth must be at least 3"): + Drain(depth=2) + + +class TestMasking: + def test_mask_renders_as_named_slot(self): + miner = TemplateMiner(TemplateMinerConfig(masking_instructions=(MaskingInstruction(r"\d+", "NUM"),))) + cluster = miner.add_log_message("took 15 ms") + + assert cluster.get_template() == "took ms" + + def test_masks_apply_in_order(self): + # The IP mask runs first, so the NUM mask never sees the octets. + instructions = ( + MaskingInstruction(r"\d{1,3}(?:\.\d{1,3}){3}", "IP"), + MaskingInstruction(r"\d+", "NUM"), + ) + miner = TemplateMiner(TemplateMinerConfig(masking_instructions=instructions)) + + assert miner.add_log_message("from 10.0.0.1 port 22").get_template() == "from port " + + +class TestClusterStore: + def _cluster(self, cluster_id: int) -> LogCluster: + return LogCluster(["t"], cluster_id) + + def test_unbounded_store_keeps_everything(self): + store = ClusterStore() + for cluster_id in range(1, 101): + store[cluster_id] = self._cluster(cluster_id) + + assert len(store) == 100 + + def test_evicts_least_recently_used(self): + store = ClusterStore(maxsize=2) + store[1] = self._cluster(1) + store[2] = self._cluster(2) + store.touch(1) + store[3] = self._cluster(3) + + assert 2 not in store + assert 1 in store + assert 3 in store + + def test_get_does_not_refresh_recency(self): + store = ClusterStore(maxsize=2) + store[1] = self._cluster(1) + store[2] = self._cluster(2) + assert store.get(1) is not None + store[3] = self._cluster(3) + + assert store.get(1) is None + assert store.get(2) is not None + + def test_replacing_a_stored_id_evicts_nothing(self): + store = ClusterStore(maxsize=2) + store[1] = self._cluster(1) + store[2] = self._cluster(2) + store[1] = self._cluster(1) + + assert len(store) == 2 + + @pytest.mark.parametrize("maxsize", [0, -1]) + def test_maxsize_below_one_is_rejected(self, maxsize): + with pytest.raises(ValueError, match="maxsize must be at least 1"): + ClusterStore(maxsize=maxsize) + + def test_miner_get_cluster_returns_none_once_evicted(self): + miner = TemplateMiner(TemplateMinerConfig(drain_max_clusters=1)) + first = miner.add_log_message("a b") + second = miner.add_log_message("c d e") + + assert miner.get_cluster(first.cluster_id) is None + assert miner.get_cluster(second.cluster_id) is second + + +class TestNoReplacedDependency: + def test_importing_logreducer_loads_none_of_the_replaced_packages(self): + code = ( + "import sys, logreducer, logreducer.core, logreducer.patterns, logreducer.temporal\n" + "print(sorted(m for m in sys.modules if m.split('.')[0] in " + f"{sorted(REPLACED)!r}))" + ) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=True, + ) + + assert result.stdout.strip() == "[]" + + def test_distribution_declares_none_of_the_replaced_packages(self): + requirements = importlib.metadata.requires("logreducer") or [] + names = {re.split(r"[\s;<>=!~\[(]", req, maxsplit=1)[0].lower() for req in requirements} + + assert names.isdisjoint(REPLACED) diff --git a/uv.lock b/uv.lock index e8384df..c18dfa8 100644 --- a/uv.lock +++ b/uv.lock @@ -125,15 +125,6 @@ filecache = [ { name = "filelock" }, ] -[[package]] -name = "cachetools" -version = "4.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/17/5735dd9f015f03d2d928ea108f3c02075b784ceed05d32a98e7e44ddd114/cachetools-4.2.1.tar.gz", hash = "sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9", size = 24753, upload-time = "2021-01-24T22:40:13.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/72/8df2e0dc991f1a1d2c6869404e7622e8ee50d80bff357dbb57c3df70305b/cachetools-4.2.1-py3-none-any.whl", hash = "sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2", size = 12003, upload-time = "2021-01-24T22:40:11.795Z" }, -] - [[package]] name = "certifi" version = "2026.7.22" @@ -566,16 +557,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/23/529140fe1aab80fc6992f93a706deec709140a6397439139a054e1515c45/docker-7.2.0-py3-none-any.whl", hash = "sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f", size = 148775, upload-time = "2026-07-09T14:53:45.224Z" }, ] -[[package]] -name = "drain3" -version = "0.9.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachetools" }, - { name = "jsonpickle" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/83/4da2d3a11b5e0edf1a4f4c0c2dd42126d2eb1f31c733967edd3dfac1af94/drain3-0.9.11.tar.gz", hash = "sha256:9ab4b1407fad74f56554ae371ef019c3c7985861631f4bab46a0e92585125f75", size = 27960, upload-time = "2022-07-17T06:40:11.433Z" } - [[package]] name = "filelock" version = "4.0.12" @@ -615,15 +596,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/53/84099323c2ec4be98d935f63c033ac4151ee83836ca1050ede3b3aadf155/joblib-1.6.0-py3-none-any.whl", hash = "sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba", size = 306115, upload-time = "2026-08-31T09:39:02.298Z" }, ] -[[package]] -name = "jsonpickle" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/62/31ef0b58050a3731b079af69932104a9443bff07fe2b9564c161e3ec4348/jsonpickle-1.5.1.tar.gz", hash = "sha256:060f97096559d1b86aa16cac2f4ea5f7b6da0c15d8a4de150b78013a886f9a51", size = 109560, upload-time = "2021-01-31T05:57:15.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a7/c2f527ddce3155ae9e008385963c2325cbfd52969f8b38efa2723e2af4af/jsonpickle-1.5.1-py2.py3-none-any.whl", hash = "sha256:8eb8323f0e12cb40687f0445e2115d8165901e20ac670add55bb53a95c68c0e5", size = 37124, upload-time = "2021-01-31T05:57:12.256Z" }, -] - [[package]] name = "librt" version = "0.16.0" @@ -711,8 +683,6 @@ name = "logreducer" version = "3.4.0" source = { editable = "." } dependencies = [ - { name = "drain3" }, - { name = "loguru" }, { name = "numpy" }, { name = "psutil" }, { name = "scikit-learn" }, @@ -756,15 +726,13 @@ dev = [ [package.metadata] requires-dist = [ - { name = "clickhouse-connect", marker = "extra == 'clickhouse'", specifier = ">=1.0" }, - { name = "confluent-kafka", marker = "extra == 'kafka'", specifier = ">=2.14.0" }, + { name = "clickhouse-connect", marker = "extra == 'clickhouse'", specifier = ">=1.9.0" }, + { name = "confluent-kafka", marker = "extra == 'kafka'", specifier = ">=2.15.1" }, { name = "datasketch", marker = "extra == 'enhanced'", specifier = ">=2.0.0" }, - { name = "drain3", specifier = ">=0.9.0" }, - { name = "loguru", specifier = ">=0.7.3" }, - { name = "numpy", specifier = ">=2.0.0" }, - { name = "psutil", specifier = ">=7.2.0" }, - { name = "scikit-learn", specifier = ">=1.5.0" }, - { name = "sqlalchemy", marker = "extra == 'sql'", specifier = ">=2.0.30" }, + { name = "numpy", specifier = ">=2.5.3" }, + { name = "psutil", specifier = ">=7.2.2" }, + { name = "scikit-learn", specifier = ">=1.9.1" }, + { name = "sqlalchemy", marker = "extra == 'sql'", specifier = ">=2.1.3" }, { name = "typer", specifier = ">=0.27.3" }, { name = "xxhash", marker = "extra == 'enhanced'", specifier = ">=4.0.1" }, ] @@ -773,8 +741,8 @@ provides-extras = ["enhanced", "sql", "clickhouse", "kafka"] [package.metadata.requires-dev] dev = [ { name = "bandit", extras = ["toml"], specifier = ">=1.7.0" }, - { name = "clickhouse-connect", specifier = ">=1.0" }, - { name = "confluent-kafka", specifier = ">=2.14.0" }, + { name = "clickhouse-connect", specifier = ">=1.9.0" }, + { name = "confluent-kafka", specifier = ">=2.15.1" }, { name = "mypy", specifier = ">=2.0" }, { name = "pip-audit", specifier = ">=2.6.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2" }, @@ -783,26 +751,13 @@ dev = [ { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "ruff", specifier = ">=0.16.10" }, - { name = "sqlalchemy", specifier = ">=2.0.30" }, + { name = "sqlalchemy", specifier = ">=2.1.3" }, { name = "testcontainers", extras = ["clickhouse", "kafka", "postgres", "mysql"], specifier = ">=4.15.0" }, { name = "ty", specifier = ">=0.0.34" }, { name = "vermin", specifier = ">=1.6.0" }, { name = "vulture", specifier = ">=2.16" }, ] -[[package]] -name = "loguru" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, -] - [[package]] name = "lz4" version = "4.4.5" @@ -1702,15 +1657,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/be/f935130312330614811dae2ea9df3f395f6d63889eb6c2e68c14507152ee/vulture-2.16-py3-none-any.whl", hash = "sha256:6e0f1c312cef1c87856957e5c2ca9608834a7c794c2180477f30bf0e4cc58eee", size = 26993, upload-time = "2026-03-25T14:41:26.21Z" }, ] -[[package]] -name = "win32-setctime" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, -] - [[package]] name = "wrapt" version = "2.5.0" From 61e3008bf2903c6302c31c8f56d5a9acd8d9491b Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 14 Oct 2026 18:16:23 +1100 Subject: [PATCH 2/3] docs: document Drain.max_node_depth --- src/logreducer/template_mining.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/logreducer/template_mining.py b/src/logreducer/template_mining.py index 065d9d9..3faba00 100644 --- a/src/logreducer/template_mining.py +++ b/src/logreducer/template_mining.py @@ -146,6 +146,7 @@ class Drain: holds the clusters a line is scored against. Attributes: + max_node_depth: Depth of the leaf nodes, counting the token-count level as 1. sim_th: Minimum share of matching tokens to join an existing cluster. max_children: Children a tree node holds before new tokens share ``<*>``. root_node: Root of the prefix tree. From 1dc381efe14347c9f41280afb39a160143b38495 Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 14 Oct 2026 18:29:30 +1100 Subject: [PATCH 3/3] docs: point the template_mining docstring at the header and README credits --- src/logreducer/template_mining.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logreducer/template_mining.py b/src/logreducer/template_mining.py index 3faba00..372c1a6 100644 --- a/src/logreducer/template_mining.py +++ b/src/logreducer/template_mining.py @@ -40,8 +40,8 @@ Derived from Drain3 at commit 0fb6d6e1828838fa05c8f6fa5cda878e02ea79c7. It keeps only the streaming path logreducer drives: mask a line, route it through a fixed-depth prefix tree, and merge it into the most similar cluster of the same -token count, widening differing tokens to ``<*>``. NOTICE lists what changed -from upstream. +token count, widening differing tokens to ``<*>``. Drain3's copyright and MIT +licence notice are in the header above; the README credits Drain3 and logparser. """ import re