|
| 1 | +defmodule Geo.LogTrimmer do |
| 2 | + @moduledoc """ |
| 3 | + A GenServer that periodically trims log files to prevent them from growing too large. |
| 4 | +
|
| 5 | + This process runs in the background and periodically checks the specified log file. |
| 6 | + If the file exceeds the maximum number of lines, it trims it to keep only the most recent lines. |
| 7 | + """ |
| 8 | + |
| 9 | + use GenServer |
| 10 | + require Logger |
| 11 | + |
| 12 | + @trim_interval :timer.minutes(5) |
| 13 | + @max_log_lines 10_000 |
| 14 | + |
| 15 | + # Client API |
| 16 | + |
| 17 | + @doc """ |
| 18 | + Starts the log trimmer for the specified log file. |
| 19 | + """ |
| 20 | + def start_link(log_file) when is_binary(log_file) do |
| 21 | + GenServer.start_link(__MODULE__, log_file, name: __MODULE__) |
| 22 | + end |
| 23 | + |
| 24 | + @doc """ |
| 25 | + Stops the log trimmer. |
| 26 | + """ |
| 27 | + def stop do |
| 28 | + if Process.whereis(__MODULE__) do |
| 29 | + GenServer.stop(__MODULE__) |
| 30 | + end |
| 31 | + end |
| 32 | + |
| 33 | + @doc """ |
| 34 | + Manually triggers a log trim operation. |
| 35 | + """ |
| 36 | + def trim_now do |
| 37 | + if Process.whereis(__MODULE__) do |
| 38 | + GenServer.cast(__MODULE__, :trim_now) |
| 39 | + end |
| 40 | + end |
| 41 | + |
| 42 | + @doc """ |
| 43 | + Gets the current status of the log trimmer. |
| 44 | + """ |
| 45 | + def status do |
| 46 | + if Process.whereis(__MODULE__) do |
| 47 | + GenServer.call(__MODULE__, :status) |
| 48 | + else |
| 49 | + {:error, :not_running} |
| 50 | + end |
| 51 | + end |
| 52 | + |
| 53 | + # Server Callbacks |
| 54 | + |
| 55 | + @impl GenServer |
| 56 | + def init(log_file) do |
| 57 | + # Schedule the first trim |
| 58 | + schedule_trim() |
| 59 | + |
| 60 | + state = %{ |
| 61 | + log_file: log_file, |
| 62 | + last_trim: DateTime.utc_now(), |
| 63 | + trim_count: 0 |
| 64 | + } |
| 65 | + |
| 66 | + Logger.info("LogTrimmer started for #{log_file}, trimming every #{div(@trim_interval, 60_000)} minutes") |
| 67 | + |
| 68 | + {:ok, state} |
| 69 | + end |
| 70 | + |
| 71 | + @impl GenServer |
| 72 | + def handle_info(:trim, state) do |
| 73 | + new_state = perform_trim(state) |
| 74 | + schedule_trim() |
| 75 | + {:noreply, new_state} |
| 76 | + end |
| 77 | + |
| 78 | + @impl GenServer |
| 79 | + def handle_cast(:trim_now, state) do |
| 80 | + new_state = perform_trim(state) |
| 81 | + {:noreply, new_state} |
| 82 | + end |
| 83 | + |
| 84 | + @impl GenServer |
| 85 | + def handle_call(:status, _from, state) do |
| 86 | + status = %{ |
| 87 | + log_file: state.log_file, |
| 88 | + last_trim: state.last_trim, |
| 89 | + trim_count: state.trim_count, |
| 90 | + max_lines: @max_log_lines, |
| 91 | + trim_interval_minutes: div(@trim_interval, 60_000) |
| 92 | + } |
| 93 | + {:reply, {:ok, status}, state} |
| 94 | + end |
| 95 | + |
| 96 | + # Private Functions |
| 97 | + |
| 98 | + defp schedule_trim do |
| 99 | + Process.send_after(self(), :trim, @trim_interval) |
| 100 | + end |
| 101 | + |
| 102 | + defp perform_trim(state) do |
| 103 | + case trim_log_file(state.log_file) do |
| 104 | + {:ok, :trimmed, lines_removed} -> |
| 105 | + Logger.debug("LogTrimmer: Trimmed #{lines_removed} lines from #{state.log_file}") |
| 106 | + %{state | last_trim: DateTime.utc_now(), trim_count: state.trim_count + 1} |
| 107 | + |
| 108 | + {:ok, :no_trim_needed} -> |
| 109 | + %{state | last_trim: DateTime.utc_now()} |
| 110 | + |
| 111 | + {:error, reason} -> |
| 112 | + Logger.warning("LogTrimmer: Failed to trim #{state.log_file}: #{reason}") |
| 113 | + state |
| 114 | + end |
| 115 | + end |
| 116 | + |
| 117 | + defp trim_log_file(log_file) do |
| 118 | + try do |
| 119 | + if File.exists?(log_file) do |
| 120 | + content = File.read!(log_file) |
| 121 | + lines = String.split(content, "\n") |
| 122 | + line_count = length(lines) |
| 123 | + |
| 124 | + if line_count > @max_log_lines do |
| 125 | + # Keep only the last @max_log_lines lines |
| 126 | + trimmed_lines = lines |> Enum.take(-@max_log_lines) |
| 127 | + trimmed_content = Enum.join(trimmed_lines, "\n") |
| 128 | + |
| 129 | + # Write to a temporary file and rename for atomic operation |
| 130 | + temp_file = log_file <> ".tmp" |
| 131 | + File.write!(temp_file, trimmed_content) |
| 132 | + File.rename!(temp_file, log_file) |
| 133 | + |
| 134 | + lines_removed = line_count - @max_log_lines |
| 135 | + {:ok, :trimmed, lines_removed} |
| 136 | + else |
| 137 | + {:ok, :no_trim_needed} |
| 138 | + end |
| 139 | + else |
| 140 | + {:ok, :no_trim_needed} |
| 141 | + end |
| 142 | + rescue |
| 143 | + error -> |
| 144 | + {:error, Exception.message(error)} |
| 145 | + end |
| 146 | + end |
| 147 | +end |
0 commit comments