Skip to content

Proyecto hilo: loganalyzer (CLI de análisis de logs) - #6

Merged
dcarrascosa merged 7 commits into
mainfrom
feat/proyecto-hilo
May 13, 2026
Merged

Proyecto hilo: loganalyzer (CLI de análisis de logs)#6
dcarrascosa merged 7 commits into
mainfrom
feat/proyecto-hilo

Conversation

@dcarrascosa

@dcarrascosadcarrascosa commented May 13, 2026

Copy link
Copy Markdown
Owner

Resumen

Bloque 3 del rediseño: añade el proyecto hilo loganalyzer como paquete Python instalable bajo proyecto/. Es la pieza que da continuidad al curso — cada módulo aporta una funcionalidad concreta sobre este proyecto.

Cambios

  • proyecto/pyproject.toml: paquete instalable con hatchling, entry point loganalyzer, deps de dev (pytest, ruff).
  • proyecto/src/loganalyzer/:
    • __init__.py: API pública.
    • parser.py: LogEntry (dataclass frozen) + parse_line + parse_file (lectura lazy).
    • filters.py: filter_by_level (severidad) + filter_by_pattern (regex).
    • reporter.py: Summary + summarize (Counter por nivel/fuente/top mensajes) + format_summary.
    • cli.py: punto de entrada con argparse. Flags --level, --match, --top.
  • proyecto/tests/: 4 ficheros de test cubriendo parser, filters, reporter y CLI con fixtures tmp_path / capsys.
  • proyecto/samples/app.log: log sintético para probar la CLI sin tener que generar uno.
  • proyecto/README.md: documentación de uso, estructura y roadmap por módulo del curso.

Decisiones

  • Cero dependencias en runtime — todo con stdlib (argparse, re, dataclasses, collections). Las únicas deps son de dev (pytest, ruff).
  • Lectura lazy en parse_file con yield — permite analizar logs grandes sin cargar a memoria.
  • Filtros como generadores — encadenables sin construir listas intermedias.
  • Summary es dataclass(frozen=True) para que sea hashable e inmutable.

Plan de test

  • cd proyecto && uv sync --group dev && uv run pytest -v → todos los tests verdes.
  • uv run loganalyzer samples/app.log → muestra el resumen.
  • uv run loganalyzer samples/app.log --level ERROR → filtra al nivel.
  • uv run loganalyzer samples/app.log --match postgres → filtra por regex.
  • CI del workflow del bloque 4 corre estos tests automáticamente.

@sourcery-ai

sourcery-aiBot commented May 13, 2026

Copy link
Copy Markdown

Guía para revisores

Añade el nuevo paquete instalable de Python loganalyzer bajo proyecto/ que implementa un analizador de logs vía CLI con parseo lazy, filtros componibles, generación de informes, cobertura completa con pytest, además de documentación y metadatos de empaquetado.

Diagrama de secuencia para el punto de entrada CLI de loganalyzer

sequenceDiagram
actor User
participant CLI as cli_main
participant Parser as parse_file
participant Filters as filter_functions
participant Reporter as summarize_format_summary
User->>CLI: main(argv)
CLI->>CLI: _build_parser()
CLI->>CLI: parser.parse_args(argv)
CLI->>CLI: Path.exists()
alt fichero no existe
CLI-->>User: stderr Fichero no encontrado
CLI-->>User: return 1
else fichero existe
CLI->>Parser: parse_file(fichero)
Note over CLI,Filters: entries es un iterador lazy
opt level proporcionado
CLI->>Filters: filter_by_level(entries, level)
Filters-->>CLI: entries_filtrados
end
opt match proporcionado
CLI->>Filters: filter_by_pattern(entries, pattern)
Filters-->>CLI: entries_filtrados
end
CLI->>Reporter: summarize(entries, top_n)
Reporter-->>CLI: Summary
CLI->>Reporter: format_summary(Summary)
Reporter-->>CLI: texto_resumen
CLI-->>User: imprime resumen
CLI-->>User: return 0
end
Loading

Cambios a nivel de archivos

CambioDetallesArchivos
Introduce el paquete instalable loganalyzer con punto de entrada CLI y configuración de empaquetado.
  • Define los metadatos del proyecto, el script de entrada, las dependencias de desarrollo y la configuración de build usando hatchling en la configuración de pyproject.
  • Expone loganalyzer.cli:main como el script de consola loganalyzer.
  • Configura Ruff y pytest (testpaths) para el subproyecto proyecto.
proyecto/pyproject.toml
Implementa el parseo de logs principal, el filtrado, la generación de resúmenes y la superficie del API público.
  • Añade la dataclass inmutable LogEntry y ayudas de parseo (parse_line, parse_file lazy) basadas en un formato de log con regex estricta.
  • Implementa filtros basados en nivel y en regex como generadores con un mapa de severidad y validación.
  • Implementa la dataclass Summary más la lógica de agregación summarize y el pretty-printer format_summary, usando Counters internamente.
  • Reexporta los tipos y funciones principales desde __init__.py y define __version__ y __all__ para el API público.
proyecto/src/loganalyzer/parser.py
proyecto/src/loganalyzer/filters.py
proyecto/src/loganalyzer/reporter.py
proyecto/src/loganalyzer/__init__.py
Añade el punto de entrada CLI que conecta el parseo, filtrado e informes con el parseo de argumentos y el manejo de errores.
  • Define un _build_parser basado en argparse con un fichero de log posicional y opciones --level, --match y --top.
  • Implementa main(argv) para comprobar la existencia del fichero, construir la canalización lazy (parseo + filtros), resumir con un N máximo configurable y mostrar el resumen formateado.
  • Devuelve códigos de salida explícitos (0 en caso de éxito, 1 si falta el fichero) y proporciona la guarda if __name__ == '__main__'.
proyecto/src/loganalyzer/cli.py
Proporciona datos de ejemplo, documentación y tests que validan la nueva funcionalidad.
  • Añade un README que documenta la instalación, el uso de la CLI, la salida de ejemplo, la estructura del proyecto, la hoja de ruta módulo por módulo y los comandos para ejecutar tests.
  • Incluye un samples/app.log sintético para ejercitar la CLI sin necesidad de logs proporcionados por el usuario.
  • Añade suites de pytest para el parser, filtros, reporter y CLI, incluyendo fixtures que usan tmp_path/capsys y tests para casos exitosos, formatos no válidos, comportamiento de filtrado y salidas por error.
proyecto/README.md
proyecto/samples/app.log
proyecto/tests/test_parser.py
proyecto/tests/test_filters.py
proyecto/tests/test_reporter.py
proyecto/tests/test_cli.py

Consejos y comandos

Interacción con Sourcery

  • Lanzar una nueva revisión: Comenta @sourcery-ai review en el pull request.
  • Continuar discusiones: Responde directamente a los comentarios de revisión de Sourcery.
  • Generar un issue de GitHub a partir de un comentario de revisión: Pídele a Sourcery que cree un issue a partir de un comentario de revisión respondiendo a él. También puedes responder a un comentario de revisión con @sourcery-ai issue para crear un issue a partir de ese comentario.
  • Generar un título para el pull request: Escribe @sourcery-ai en cualquier parte del título del pull request para generar un título en cualquier momento. También puedes comentar @sourcery-ai title en el pull request para (re)generar el título en cualquier momento.
  • Generar un resumen del pull request: Escribe @sourcery-ai summary en cualquier parte del cuerpo del pull request para generar un resumen del PR en cualquier momento exactamente donde lo quieras. También puedes comentar @sourcery-ai summary en el pull request para (re)generar el resumen en cualquier momento.
  • Generar la guía para revisores: Comenta @sourcery-ai guide en el pull request para (re)generar la guía para revisores en cualquier momento.
  • Resolver todos los comentarios de Sourcery: Comenta @sourcery-ai resolve en el pull request para marcar como resueltos todos los comentarios de Sourcery. Es útil si ya has abordado todos los comentarios y no quieres verlos más.
  • Descartar todas las revisiones de Sourcery: Comenta @sourcery-ai dismiss en el pull request para descartar todas las revisiones existentes de Sourcery. Es especialmente útil si quieres empezar de cero con una nueva revisión; no olvides comentar @sourcery-ai review para lanzar una nueva revisión.

Personalizar tu experiencia

Accede a tu panel de control para:

  • Activar o desactivar funciones de revisión como el resumen del pull request generado por Sourcery, la guía para revisores y otras.
  • Cambiar el idioma de revisión.
  • Añadir, eliminar o editar instrucciones de revisión personalizadas.
  • Ajustar otras opciones de revisión.

Obtener ayuda

Original review guide in English

Reviewer's Guide

Adds the new installable Python package loganalyzer under proyecto/ implementing a CLI log analyzer with lazy parsing, composable filters, reporting, and full pytest coverage plus documentation and packaging metadata.

Sequence diagram for the loganalyzer CLI entrypoint

sequenceDiagram
actor User
participant CLI as cli_main
participant Parser as parse_file
participant Filters as filter_functions
participant Reporter as summarize_format_summary
User->>CLI: main(argv)
CLI->>CLI: _build_parser()
CLI->>CLI: parser.parse_args(argv)
CLI->>CLI: Path.exists()
alt fichero no existe
CLI-->>User: stderr Fichero no encontrado
CLI-->>User: return 1
else fichero existe
CLI->>Parser: parse_file(fichero)
Note over CLI,Filters: entries es un iterador lazy
opt level proporcionado
CLI->>Filters: filter_by_level(entries, level)
Filters-->>CLI: entries_filtrados
end
opt match proporcionado
CLI->>Filters: filter_by_pattern(entries, pattern)
Filters-->>CLI: entries_filtrados
end
CLI->>Reporter: summarize(entries, top_n)
Reporter-->>CLI: Summary
CLI->>Reporter: format_summary(Summary)
Reporter-->>CLI: texto_resumen
CLI-->>User: imprime resumen
CLI-->>User: return 0
end
Loading

File-Level Changes

ChangeDetailsFiles
Introduce installable loganalyzer package with CLI entry point and packaging config.
  • Define project metadata, script entry point, dev dependencies, and build config using hatchling in pyproject configuration.
  • Expose loganalyzer.cli:main as the loganalyzer console script.
  • Configure Ruff and pytest (testpaths) for the proyecto subproject.
proyecto/pyproject.toml
Implement core log parsing, filtering, summarization, and public API surface.
  • Add frozen LogEntry dataclass and parsing helpers (parse_line, lazy parse_file) based on a strict regex log format.
  • Implement level-based and regex-based filters as generators with a severity map and validation.
  • Implement Summary dataclass plus summarize aggregation logic and format_summary pretty-printer, using Counters internally.
  • Re-export main types and functions from __init__.py and define __version__ and __all__ for the public API.
proyecto/src/loganalyzer/parser.py
proyecto/src/loganalyzer/filters.py
proyecto/src/loganalyzer/reporter.py
proyecto/src/loganalyzer/__init__.py
Add CLI entry point that wires parsing, filtering, and reporting with argument parsing and error handling.
  • Define an argparse-based _build_parser with positional log file, --level, --match, and --top options.
  • Implement main(argv) to check file existence, build the lazy pipeline (parse + filters), summarize with configurable top N, and print the formatted summary.
  • Return explicit exit codes (0 on success, 1 on missing file) and provide the if __name__ == '__main__' guard.
proyecto/src/loganalyzer/cli.py
Provide sample data, documentation, and tests validating the new functionality.
  • Add README documenting installation, CLI usage, sample output, project structure, module-by-module roadmap, and test execution commands.
  • Ship a synthetic samples/app.log to exercise the CLI without user-provided logs.
  • Add pytest suites for parser, filters, reporter, and CLI, including fixtures using tmp_path/capsys and tests for happy paths, invalid formats, filtering behavior, and error exits.
proyecto/README.md
proyecto/samples/app.log
proyecto/tests/test_parser.py
proyecto/tests/test_filters.py
proyecto/tests/test_reporter.py
proyecto/tests/test_cli.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@dcarrascosa
dcarrascosa merged commit d9cc7ef into mainMay 13, 2026
1 check passed

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, he encontrado 4 problemas y he dejado algunos comentarios de alto nivel:

  • En summarize, convertir todo el iterable entries en una lista va en contra del diseño perezoso y por streaming de parse_file y de los filtros; plantéate calcular los contadores en una sola pasada sobre el iterador para mantener el uso de memoria acotado en logs grandes.
  • En cli.main, la variable entries se puede tipar simplemente como Iterable[LogEntry] en lugar de Iterable[LogEntry] | Iterator[LogEntry], ya que Iterator es un subtipo de Iterable y la unión no aporta valor.
Prompt para agentes de IA
Por favor, atiende a los comentarios de esta revisión de código:
## Comentarios generales- En `summarize`, convertir todo el iterable `entries` en una lista va en contra del diseño perezoso y por streaming de `parse_file` y de los filtros; plantéate calcular los contadores en una sola pasada sobre el iterador para mantener el uso de memoria acotado en logs grandes.
- En `cli.main`, la variable `entries` se puede tipar simplemente como `Iterable[LogEntry]` en lugar de `Iterable[LogEntry] | Iterator[LogEntry]`, ya que `Iterator` es un subtipo de `Iterable` y la unión no aporta valor.
## Comentarios individuales### Comentario 1
<locationpath="proyecto/src/loganalyzer/reporter.py"line_range="22-23" />
<code_context>
+
+def summarize(entries: Iterable[LogEntry], top_n: int = 5) -> Summary:
+ """Calcula el resumen agregado de las entradas."""
+ entries_list = list(entries)
+ return Summary(
+ total=len(entries_list),+ by_level=dict(Counter(e.level for e in entries_list)),+ by_source=dict(Counter(e.source for e in entries_list)),+ top_messages=Counter(e.message for e in entries_list).most_common(top_n),+ )
+
</code_context>
<issue_to_address>
**suggestion (performance):** Evita materializar todas las entradas en una lista para reducir el uso de memoria y mejorar la escalabilidad.
Construir `entries_list` fuerza a cargar en memoria todo el stream de logs, lo que no escala bien para entradas grandes. En su lugar, itera una sola vez sobre `entries` y mantén `Counter`s y un total acumulado sobre la marcha, y luego deriva `top_messages` a partir de `by_message.most_common(top_n)`. Esto mantiene la memoria proporcional al número de claves distintas en lugar del número total de entradas.
```suggestiondef summarize(entries: Iterable[LogEntry], top_n: int = 5) -> Summary: """Calcula el resumen agregado de las entradas sin materializar todo el stream.""" by_level: Counter[str] = Counter() by_source: Counter[str] = Counter() by_message: Counter[str] = Counter() total = 0 for entry in entries: total += 1 by_level[entry.level] += 1 by_source[entry.source] += 1 by_message[entry.message] += 1 return Summary( total=total, by_level=dict(by_level), by_source=dict(by_source), top_messages=by_message.most_common(top_n), )```
</issue_to_address>
### Comentario 2
<locationpath="proyecto/src/loganalyzer/cli.py"line_range="51-52" />
<code_context>
+ entries: Iterable[LogEntry] | Iterator[LogEntry] = parse_file(args.fichero)
+ if args.level:
+ entries = filter_by_level(entries, args.level)+ if args.match:
+ entries = filter_by_pattern(entries, args.match)++ summary = summarize(entries, top_n=args.top)
</code_context>
<issue_to_address>
**issue (bug_risk):** Gestiona los patrones regex no válidos de forma elegante para evitar que la CLI se bloquee con un traceback.
Como `pattern` proviene de la entrada del usuario, `re.compile` en `filter_by_pattern` puede lanzar `re.error` para expresiones no válidas, lo que actualmente se propaga y muestra un stack trace. Plantéate capturar `re.error` alrededor de la llamada a `filter_by_pattern` en `main` y salir con un mensaje de error claro y un código de estado distinto de cero.
</issue_to_address>
### Comentario 3
<locationpath="proyecto/src/loganalyzer/parser.py"line_range="49-52" />
<code_context>
+ Lectura lazy: cada línea se procesa al vuelo, sin cargar el fichero entero
+ en memoria.
+ """
+ with open(ruta, encoding="utf-8") as f:
+ for linea in f:+ entry = parse_line(linea)+ if entry is not None:+ yield entry
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Plantéate manejar errores de E/S al abrir el fichero de logs para producir mensajes de error más claros en la CLI.
Dado que `parse_file` se llama desde la CLI, los `OSError` sin manejar que se produzcan en `open` actualmente se muestran como un stack trace. Plantéate capturarlos en el punto de llamada y devolver un error claro y un código de salida distinto de cero, por ejemplo:
```pythontry:
entries = parse_file(args.fichero)
exceptOSErroras exc:
print(f"No se pudo leer el fichero {args.fichero}: {exc}", file=sys.stderr)
return1```
Esto complementa la comprobación de existencia actual y mejora la robustez.
</issue_to_address>
### Comentario 4
<locationpath="proyecto/tests/test_cli.py"line_range="24-31" />
<code_context>
+ return path
++
+def test_main_runs_and_prints_summary(
+ sample_log: Path, capsys: pytest.CaptureFixture[str]
+) -> None:
+ exit_code = main([str(sample_log)])
+ assert exit_code == 0
+ output = capsys.readouterr().out
+# 3 válidas, 1 descartada por formato+ assert "Total de entradas: 3" in output
++
</code_context>
<issue_to_address>
**suggestion (testing):** Añade tests de CLI para la opción `--top` y para combinaciones de filtros / resultados vacíos.
Los tests de CLI actuales cubren la ejecución básica y los filtros más comunes, pero todavía no ejercitan `--top` ni los escenarios con resultados vacíos. Por favor, añade (1) un test que invoque `--top` con una distribución conocida de mensajes repetidos y compruebe que el número de líneas bajo `Top N mensajes:` coincide con el valor solicitado, y (2) un test en el que los filtros no devuelvan coincidencias (por ejemplo, `--level CRITICAL` sobre un log sin entradas CRITICAL) y verifique que el resumen imprime `Total de entradas: 0`. Esto confirmará que `top` se pasa correctamente a `summarize` y que la CLI gestiona correctamente los casos sin coincidencias.
```suggestiondef test_main_runs_and_prints_summary( sample_log: Path, capsys: pytest.CaptureFixture[str]) -> None: exit_code = main([str(sample_log)]) assert exit_code == 0 output = capsys.readouterr().out # 3 válidas, 1 descartada por formato assert "Total de entradas: 3" in outputdef test_main_top_flag_limits_number_of_top_messages( sample_log: Path, capsys: pytest.CaptureFixture[str]) -> None: exit_code = main([str(sample_log), "--top", "2"]) assert exit_code == 0 output = capsys.readouterr().out lines = output.splitlines() # Encontrar la línea de cabecera del top N header_index = None for i, line in enumerate(lines): if "Top 2 mensajes" in line: header_index = i break assert header_index is not None, "No se encontró la cabecera de 'Top 2 mensajes' en la salida del CLI" # Contar las líneas no vacías siguientes hasta el próximo bloque (línea en blanco) top_lines_count = 0 for line in lines[header_index + 1 :]: if not line.strip(): break top_lines_count += 1 assert top_lines_count == 2def test_main_with_filters_yielding_no_matches_prints_zero_entries( sample_log: Path, capsys: pytest.CaptureFixture[str]) -> None: # No hay entradas CRITICAL en el log de ejemplo, así que los filtros no deberían devolver nada exit_code = main([str(sample_log), "--level", "CRITICAL"]) assert exit_code == 0 output = capsys.readouterr().out assert "Total de entradas: 0" in output```
</issue_to_address>

Sourcery es gratis para open source: si te gustan nuestras revisiones, por favor piensa en compartirlas ✨
¡Ayúdame a ser más útil! Por favor, haz clic en 👍 o 👎 en cada comentario y usaré el feedback para mejorar tus revisiones.
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • In summarize, converting the entire entries iterable to a list defeats the lazy, streaming design of parse_file and the filters; consider computing the counters in a single pass over the iterator to keep memory usage bounded for large logs.
  • In cli.main, the entries variable can simply be typed as Iterable[LogEntry] instead of Iterable[LogEntry] | Iterator[LogEntry], since Iterator is a subtype of Iterable and the union doesn’t add value.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `summarize`, converting the entire `entries` iterable to a list defeats the lazy, streaming design of `parse_file` and the filters; consider computing the counters in a single pass over the iterator to keep memory usage bounded for large logs.
- In `cli.main`, the `entries` variable can simply be typed as `Iterable[LogEntry]` instead of `Iterable[LogEntry] | Iterator[LogEntry]`, since `Iterator` is a subtype of `Iterable` and the union doesn’t add value.
## Individual Comments### Comment 1
<locationpath="proyecto/src/loganalyzer/reporter.py"line_range="22-23" />
<code_context>
+
+def summarize(entries: Iterable[LogEntry], top_n: int = 5) -> Summary:
+ """Calcula el resumen agregado de las entradas."""
+ entries_list = list(entries)
+ return Summary(
+ total=len(entries_list),+ by_level=dict(Counter(e.level for e in entries_list)),+ by_source=dict(Counter(e.source for e in entries_list)),+ top_messages=Counter(e.message for e in entries_list).most_common(top_n),+ )
+
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid materializing all entries into a list to reduce memory usage and improve scalability.
Building `entries_list` forces the whole log stream into memory, which doesn’t scale for large inputs. Instead, iterate once over `entries` and maintain `Counter`s and a running total as you go, then derive `top_messages` from `by_message.most_common(top_n)`. This keeps memory proportional to the number of distinct keys rather than the total number of entries.
```suggestiondef summarize(entries: Iterable[LogEntry], top_n: int = 5) -> Summary: """Calcula el resumen agregado de las entradas sin materializar todo el stream.""" by_level: Counter[str] = Counter() by_source: Counter[str] = Counter() by_message: Counter[str] = Counter() total = 0 for entry in entries: total += 1 by_level[entry.level] += 1 by_source[entry.source] += 1 by_message[entry.message] += 1 return Summary( total=total, by_level=dict(by_level), by_source=dict(by_source), top_messages=by_message.most_common(top_n), )```
</issue_to_address>
### Comment 2
<locationpath="proyecto/src/loganalyzer/cli.py"line_range="51-52" />
<code_context>
+ entries: Iterable[LogEntry] | Iterator[LogEntry] = parse_file(args.fichero)
+ if args.level:
+ entries = filter_by_level(entries, args.level)+ if args.match:
+ entries = filter_by_pattern(entries, args.match)++ summary = summarize(entries, top_n=args.top)
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle invalid regex patterns gracefully to avoid crashing the CLI with a traceback.
Because `pattern` comes from user input, `re.compile` in `filter_by_pattern` can raise `re.error` for invalid expressions, which currently bubbles up and prints a stack trace. Consider catching `re.error` around the `filter_by_pattern` call in `main` and exiting with a clear error message and non-zero status instead.
</issue_to_address>
### Comment 3
<locationpath="proyecto/src/loganalyzer/parser.py"line_range="49-52" />
<code_context>
+ Lectura lazy: cada línea se procesa al vuelo, sin cargar el fichero entero
+ en memoria.
+ """
+ with open(ruta, encoding="utf-8") as f:
+ for linea in f:+ entry = parse_line(linea)+ if entry is not None:+ yield entry
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider handling I/O errors when opening the log file to produce clearer CLI error messages.
Since `parse_file` is called from the CLI, unhandled `OSError`s from `open` will currently surface as a stack trace. Consider catching them at the call site and returning a clear error and non-zero exit code, e.g.:
```pythontry:
entries = parse_file(args.fichero)
exceptOSErroras exc:
print(f"No se pudo leer el fichero {args.fichero}: {exc}", file=sys.stderr)
return1```
This complements the existing existence check and improves robustness.
</issue_to_address>
### Comment 4
<locationpath="proyecto/tests/test_cli.py"line_range="24-31" />
<code_context>
+ return path
++
+def test_main_runs_and_prints_summary(
+ sample_log: Path, capsys: pytest.CaptureFixture[str]
+) -> None:
+ exit_code = main([str(sample_log)])
+ assert exit_code == 0
+ output = capsys.readouterr().out
+# 3 válidas, 1 descartada por formato+ assert "Total de entradas: 3" in output
++
</code_context>
<issue_to_address>
**suggestion (testing):** Add CLI tests for the `--top` flag and for combinations of filters / empty results.
Current CLI tests cover basic execution and common filters, but they don’t yet exercise `--top` or empty-result scenarios. Please add (1) a test invoking `--top` with a known distribution of repeated messages and assert the number of lines under `Top N mensajes:` matches the requested value, and (2) a test where filters yield zero matches (e.g., `--level CRITICAL` on a log without CRITICAL entries) and assert the summary prints `Total de entradas: 0`. This will verify that `top` is correctly passed to `summarize` and that the CLI handles no-match cases correctly.
```suggestiondef test_main_runs_and_prints_summary( sample_log: Path, capsys: pytest.CaptureFixture[str]) -> None: exit_code = main([str(sample_log)]) assert exit_code == 0 output = capsys.readouterr().out # 3 válidas, 1 descartada por formato assert "Total de entradas: 3" in outputdef test_main_top_flag_limits_number_of_top_messages( sample_log: Path, capsys: pytest.CaptureFixture[str]) -> None: exit_code = main([str(sample_log), "--top", "2"]) assert exit_code == 0 output = capsys.readouterr().out lines = output.splitlines() # Encontrar la línea de cabecera del top N header_index = None for i, line in enumerate(lines): if "Top 2 mensajes" in line: header_index = i break assert header_index is not None, "No se encontró la cabecera de 'Top 2 mensajes' en la salida del CLI" # Contar las líneas no vacías siguientes hasta el próximo bloque (línea en blanco) top_lines_count = 0 for line in lines[header_index + 1 :]: if not line.strip(): break top_lines_count += 1 assert top_lines_count == 2def test_main_with_filters_yielding_no_matches_prints_zero_entries( sample_log: Path, capsys: pytest.CaptureFixture[str]) -> None: # No hay entradas CRITICAL en el log de ejemplo, así que los filtros no deberían devolver nada exit_code = main([str(sample_log), "--level", "CRITICAL"]) assert exit_code == 0 output = capsys.readouterr().out assert "Total de entradas: 0" in output```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +22 to +23
def summarize(entries: Iterable[LogEntry], top_n: int = 5) -> Summary:
"""Calcula el resumen agregado de las entradas."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Evita materializar todas las entradas en una lista para reducir el uso de memoria y mejorar la escalabilidad.

Construir entries_list fuerza a cargar en memoria todo el stream de logs, lo que no escala bien para entradas grandes. En su lugar, itera una sola vez sobre entries y mantén Counters y un total acumulado sobre la marcha, y luego deriva top_messages a partir de by_message.most_common(top_n). Esto mantiene la memoria proporcional al número de claves distintas en lugar del número total de entradas.

Suggested change
defsummarize(entries: Iterable[LogEntry], top_n: int=5) ->Summary:
"""Calcula el resumen agregado de las entradas."""
defsummarize(entries: Iterable[LogEntry], top_n: int=5) ->Summary:
"""Calcula el resumen agregado de las entradas sin materializar todo el stream."""
by_level: Counter[str] =Counter()
by_source: Counter[str] =Counter()
by_message: Counter[str] =Counter()
total=0
forentryinentries:
total+=1
by_level[entry.level] +=1
by_source[entry.source] +=1
by_message[entry.message] +=1
returnSummary(
total=total,
by_level=dict(by_level),
by_source=dict(by_source),
top_messages=by_message.most_common(top_n),
)
Original comment in English

suggestion (performance): Avoid materializing all entries into a list to reduce memory usage and improve scalability.

Building entries_list forces the whole log stream into memory, which doesn’t scale for large inputs. Instead, iterate once over entries and maintain Counters and a running total as you go, then derive top_messages from by_message.most_common(top_n). This keeps memory proportional to the number of distinct keys rather than the total number of entries.

Suggested change
defsummarize(entries: Iterable[LogEntry], top_n: int=5) ->Summary:
"""Calcula el resumen agregado de las entradas."""
defsummarize(entries: Iterable[LogEntry], top_n: int=5) ->Summary:
"""Calcula el resumen agregado de las entradas sin materializar todo el stream."""
by_level: Counter[str] =Counter()
by_source: Counter[str] =Counter()
by_message: Counter[str] =Counter()
total=0
forentryinentries:
total+=1
by_level[entry.level] +=1
by_source[entry.source] +=1
by_message[entry.message] +=1
returnSummary(
total=total,
by_level=dict(by_level),
by_source=dict(by_source),
top_messages=by_message.most_common(top_n),
)

Comment on lines +51 to +52
if args.match:
entries = filter_by_pattern(entries, args.match)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Gestiona los patrones regex no válidos de forma elegante para evitar que la CLI se bloquee con un traceback.

Como pattern proviene de la entrada del usuario, re.compile en filter_by_pattern puede lanzar re.error para expresiones no válidas, lo que actualmente se propaga y muestra un stack trace. Plantéate capturar re.error alrededor de la llamada a filter_by_pattern en main y salir con un mensaje de error claro y un código de estado distinto de cero.

Original comment in English

issue (bug_risk): Handle invalid regex patterns gracefully to avoid crashing the CLI with a traceback.

Because pattern comes from user input, re.compile in filter_by_pattern can raise re.error for invalid expressions, which currently bubbles up and prints a stack trace. Consider catching re.error around the filter_by_pattern call in main and exiting with a clear error message and non-zero status instead.

Comment on lines +49 to +52
with open(ruta, encoding="utf-8") as f:
for linea in f:
entry = parse_line(linea)
if entry is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Plantéate manejar errores de E/S al abrir el fichero de logs para producir mensajes de error más claros en la CLI.

Dado que parse_file se llama desde la CLI, los OSError sin manejar que se produzcan en open actualmente se muestran como un stack trace. Plantéate capturarlos en el punto de llamada y devolver un error claro y un código de salida distinto de cero, por ejemplo:

try:
entries=parse_file(args.fichero)
exceptOSErrorasexc:
print(f"No se pudo leer el fichero {args.fichero}: {exc}", file=sys.stderr)
return1

Esto complementa la comprobación de existencia actual y mejora la robustez.

Original comment in English

suggestion (bug_risk): Consider handling I/O errors when opening the log file to produce clearer CLI error messages.

Since parse_file is called from the CLI, unhandled OSErrors from open will currently surface as a stack trace. Consider catching them at the call site and returning a clear error and non-zero exit code, e.g.:

try:
entries=parse_file(args.fichero)
exceptOSErrorasexc:
print(f"No se pudo leer el fichero {args.fichero}: {exc}", file=sys.stderr)
return1

This complements the existing existence check and improves robustness.

Comment on lines +24 to +31
def test_main_runs_and_prints_summary(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) -> None:
exit_code = main([str(sample_log)])
assert exit_code == 0
output = capsys.readouterr().out
# 3 válidas, 1 descartada por formato
assert "Total de entradas: 3" in output

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Añade tests de CLI para la opción --top y para combinaciones de filtros / resultados vacíos.

Los tests de CLI actuales cubren la ejecución básica y los filtros más comunes, pero todavía no ejercitan --top ni los escenarios con resultados vacíos. Por favor, añade (1) un test que invoque --top con una distribución conocida de mensajes repetidos y compruebe que el número de líneas bajo Top N mensajes: coincide con el valor solicitado, y (2) un test en el que los filtros no devuelvan coincidencias (por ejemplo, --level CRITICAL sobre un log sin entradas CRITICAL) y verifique que el resumen imprime Total de entradas: 0. Esto confirmará que top se pasa correctamente a summarize y que la CLI gestiona correctamente los casos sin coincidencias.

Suggested change
deftest_main_runs_and_prints_summary(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
exit_code=main([str(sample_log)])
assertexit_code==0
output=capsys.readouterr().out
# 3 válidas, 1 descartada por formato
assert"Total de entradas: 3"inoutput
deftest_main_runs_and_prints_summary(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
exit_code=main([str(sample_log)])
assertexit_code==0
output=capsys.readouterr().out
# 3 válidas, 1 descartada por formato
assert"Total de entradas: 3"inoutput
deftest_main_top_flag_limits_number_of_top_messages(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
exit_code=main([str(sample_log), "--top", "2"])
assertexit_code==0
output=capsys.readouterr().out
lines=output.splitlines()
# Encontrar la línea de cabecera del top N
header_index=None
fori, lineinenumerate(lines):
if"Top 2 mensajes"inline:
header_index=i
break
assertheader_indexisnotNone, "No se encontró la cabecera de 'Top 2 mensajes' en la salida del CLI"
# Contar las líneas no vacías siguientes hasta el próximo bloque (línea en blanco)
top_lines_count=0
forlineinlines[header_index+1 :]:
ifnotline.strip():
break
top_lines_count+=1
asserttop_lines_count==2
deftest_main_with_filters_yielding_no_matches_prints_zero_entries(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
# No hay entradas CRITICAL en el log de ejemplo, así que los filtros no deberían devolver nada
exit_code=main([str(sample_log), "--level", "CRITICAL"])
assertexit_code==0
output=capsys.readouterr().out
assert"Total de entradas: 0"inoutput
Original comment in English

suggestion (testing): Add CLI tests for the --top flag and for combinations of filters / empty results.

Current CLI tests cover basic execution and common filters, but they don’t yet exercise --top or empty-result scenarios. Please add (1) a test invoking --top with a known distribution of repeated messages and assert the number of lines under Top N mensajes: matches the requested value, and (2) a test where filters yield zero matches (e.g., --level CRITICAL on a log without CRITICAL entries) and assert the summary prints Total de entradas: 0. This will verify that top is correctly passed to summarize and that the CLI handles no-match cases correctly.

Suggested change
deftest_main_runs_and_prints_summary(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
exit_code=main([str(sample_log)])
assertexit_code==0
output=capsys.readouterr().out
# 3 válidas, 1 descartada por formato
assert"Total de entradas: 3"inoutput
deftest_main_runs_and_prints_summary(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
exit_code=main([str(sample_log)])
assertexit_code==0
output=capsys.readouterr().out
# 3 válidas, 1 descartada por formato
assert"Total de entradas: 3"inoutput
deftest_main_top_flag_limits_number_of_top_messages(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
exit_code=main([str(sample_log), "--top", "2"])
assertexit_code==0
output=capsys.readouterr().out
lines=output.splitlines()
# Encontrar la línea de cabecera del top N
header_index=None
fori, lineinenumerate(lines):
if"Top 2 mensajes"inline:
header_index=i
break
assertheader_indexisnotNone, "No se encontró la cabecera de 'Top 2 mensajes' en la salida del CLI"
# Contar las líneas no vacías siguientes hasta el próximo bloque (línea en blanco)
top_lines_count=0
forlineinlines[header_index+1 :]:
ifnotline.strip():
break
top_lines_count+=1
asserttop_lines_count==2
deftest_main_with_filters_yielding_no_matches_prints_zero_entries(
sample_log: Path, capsys: pytest.CaptureFixture[str]
) ->None:
# No hay entradas CRITICAL en el log de ejemplo, así que los filtros no deberían devolver nada
exit_code=main([str(sample_log), "--level", "CRITICAL"])
assertexit_code==0
output=capsys.readouterr().out
assert"Total de entradas: 0"inoutput

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dcarrascosa