defmodule Mix.Tasks.Log.Export do @moduledoc """ Export parsed log entries to JSON or CSV. ## Usage mix log.export [options] ## Options --to Output format: json or csv (default: json) --output Write to file instead of stdout --level Filter by log level --format Filter by log format (morgan, java, elixir, nginx) --grep Filter by message content ## Examples mix log.export logs/app.log mix log.export logs/app.log --to csv --output report.csv mix log.export logs/ --level error --to json --output errors.json mix log.export logs/app.log --to csv | head -5 """ @shortdoc "Export log entries to JSON or CSV" use Mix.Task alias TelemetryData.{FileReader, Exporter} @impl Mix.Task def run(args) do {opts, positional, _} = OptionParser.parse(args, strict: [to: :string, output: :string, level: :string, format: :string, grep: :string] ) path = List.first(positional) || "logs/" export_format = parse_export_format(Keyword.get(opts, :to, "json")) output_path = opts[:output] entries = load_entries(path) |> apply_filters(opts) if output_path do case Exporter.write_file(entries, output_path, export_format) do :ok -> Mix.shell().info("Exported #{length(entries)} entries to #{output_path}") {:error, reason} -> Mix.raise("Failed to write #{output_path}: #{reason}") end else content = case export_format do :json -> Exporter.to_json(entries) :csv -> Exporter.to_csv(entries) end IO.puts(content) end end defp parse_export_format("json"), do: :json defp parse_export_format("csv"), do: :csv defp parse_export_format(other), do: Mix.raise("Unknown format: #{other}. Use json or csv.") defp load_entries(path) do if File.dir?(path) do case FileReader.read_dir(path) do {:ok, entries} -> entries {:error, reason} -> Mix.raise("Failed to read directory #{path}: #{reason}") end else case FileReader.read_file(path) do {:ok, entries} -> entries {:error, reason} -> Mix.raise("Failed to read file #{path}: #{reason}") end end end defp apply_filters(entries, opts) do entries |> maybe_filter(:level, opts[:level]) |> maybe_filter(:format, opts[:format]) |> maybe_filter(:grep, opts[:grep]) end defp maybe_filter(entries, _key, nil), do: entries defp maybe_filter(entries, :level, level) do Enum.filter(entries, &(&1.level == String.downcase(level))) end defp maybe_filter(entries, :format, format) do Enum.filter(entries, &(&1.format == String.to_existing_atom(format))) end defp maybe_filter(entries, :grep, pattern) do pattern_down = String.downcase(pattern) Enum.filter(entries, &String.contains?(String.downcase(&1.message), pattern_down)) end end