Current section
Files
Jump to
Current section
Files
lib/log_mon.ex
defmodule LogMon do
@moduledoc "LogMon is a hat tip to logrotate."
require Logger
def run(path) when is_binary(path) do
case valid_config_path?(path) do
{:error, reason} ->
Logger.error("Failed to read config file due to: #{reason}")
config ->
result =
config
|> valid_config?()
|> check_current_log_size()
|> check_backup_count(true)
{:ok, result}
end
end
def run(config) when is_map(config) do
result =
config
|> valid_config?()
|> check_current_log_size()
|> check_backup_count(true)
{:ok, result}
end
def template_by_size() do
%{
path_to_monitor: "/full/path/filename.log",
desired_file_size: 1024,
compression: true,
max_storage_count: 4,
storage_path: "/full/path",
storage_file_name: "unique_name",
include_ts: true
}
end
def test do
%{
path_to_monitor: "/Users/nerd/logs/lucia.log",
desired_file_size: 1024,
compression: true,
max_storage_count: 4,
storage_path: "/Users/nerd/logs/backups/lucia",
storage_file_name: "test_name",
include_ts: true
}
end
# def template_by_frequency() do
# %{
# path_to_monitor: "/full/path/filename.log",
# backup_time_24_hr: ~T[04:00:00.001],
# compression: true,
# max_storage_count: 4,
# storage_path: "/full/path",
# storage_file_name: "unique_name",
# include_ts: true
# }
# end
#
defp valid_config_path?(path) do
case File.read(path) do
{:ok, file} ->
case Jason.decode(file) do
{:ok, config} ->
config
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
defp valid_config?(
%{
path_to_monitor: _path_to_monitor,
desired_file_size: _desired_file_size,
compression: _compress,
max_storage_count: _max_storage_count,
storage_path: _storage_path,
storage_file_name: _storage_file_name,
include_ts: _include_ts
} = config
) do
config
|> valid_path_to_monitor()
|> valid_file_size?()
|> valid_compression?()
|> valid_max_storage_count?()
|> valid_storage_path?()
|> valid_storage_file_name?()
|> valid_include_ts?()
|> valid_results?()
end
def valid_results?(config) do
{_pass, fail} =
config.validation
|> Enum.split_with(fn {_, parts} -> parts == :pass end)
if length(fail) > 0 do
Logger.warning("Fails found issue")
fail
|> Enum.each(fn {key, value} ->
Logger.warning("#{key} reported #{value}")
end)
end
# if length(pass) > 0 do
# results =
# Enum.map(pass, fn {key, _} ->
# # Enum.member?(
# # [
# # :path_to_monitor,
# # :desired_file_size,
# # :compression,
# # :max_storage_count,
# # :storage_path,
# # :storage_file_name
# # ],
# # key
# # )
# key
# end)
#
# IO.inspect(pass, label: "Pass")
# end
config
end
def valid_path_to_monitor(%{path_to_monitor: path_to_monitor} = config) do
case File.exists?(path_to_monitor) do
true ->
File.stat!(path_to_monitor)
log_step_result(config, :path_to_monitor, :pass)
false ->
case File.mkdir_p(path_to_monitor) do
:ok ->
log_step_result(config, :path_to_monitor, :pass)
{:error, reason} ->
{:error, "#{path_to_monitor} could not be created: #{reason}"}
log_step_result(config, :path_to_monitor, :err)
end
end
end
def valid_file_size?(%{desired_file_size: desired_file_size} = config) do
case desired_file_size do
x when is_number(x) ->
# if x > 1024 * 1024 do
if x >= 1 do
log_step_result(config, :desired_file_size, :pass)
else
log_step_result(config, :desired_file_size, :err)
end
end
end
def valid_compression?(%{compression: compression} = config) do
case is_boolean(compression) do
true -> log_step_result(config, :compression, :pass)
false -> log_step_result(config, :compression, :err)
end
end
def valid_max_storage_count?(%{desired_file_size: desired_file_size} = config) do
case desired_file_size do
x when is_number(x) ->
if x > 0 do
log_step_result(config, :max_storage_count, :pass)
else
log_step_result(config, :max_storage_count, :err)
end
end
end
def valid_storage_path?(%{storage_path: storage_path} = config) do
case File.stat(storage_path) do
{:ok, _} ->
log_step_result(config, :storage_path, :pass)
{:error, :enoent} ->
case File.mkdir_p(storage_path) do
:ok ->
log_step_result(config, :storage_path, :pass)
{:error, reason} ->
{:error, "#{storage_path} could not be created: #{reason}"}
log_step_result(config, :storage_path, :err)
end
{:error, _} ->
log_step_result(config, :storage_path, :err)
end
end
def valid_storage_file_name?(%{storage_file_name: storage_file_name} = config) do
case storage_file_name do
nil ->
log_step_result(config, :storage_file_name, :err)
"" ->
log_step_result(config, :storage_file_name, :err)
x when is_binary(x) ->
# TODO: Add cases for characters that shouldn't be in a filename?
log_step_result(config, :storage_file_name, :pass)
_ ->
log_step_result(config, :storage_file_name, :err)
end
end
def valid_include_ts?(%{include_ts: include_ts} = config) do
case is_boolean(include_ts) do
true -> log_step_result(config, :include_ts, :pass)
false -> log_step_result(config, :include_ts, :err)
end
end
defp build_storage_path(storage_path, storage_file_name, include_ts) do
case include_ts do
true ->
case DateTime.now("Etc/UTC") do
{:ok, datetime} ->
{:ok,
"#{storage_path}/#{storage_file_name}_#{datetime.year}#{datetime.month}#{datetime.day}_#{datetime.hour}#{datetime.minute}#{datetime.second}.log"}
end
false ->
{:ok, "#{storage_path}/#{storage_file_name}.log"}
end
end
defp check_current_log_size(
%{
path_to_monitor: path_to_monitor,
desired_file_size: desired_file_size,
compression: compression,
max_storage_count: _max_storage_count,
storage_path: storage_path,
storage_file_name: storage_file_name,
include_ts: include_ts
} = config
) do
case File.stat(path_to_monitor) do
{:ok, %{size: size}} ->
case size >= desired_file_size do
true ->
Logger.info("Size: #{size} >= MaxSize: #{desired_file_size}")
case build_storage_path(storage_path, storage_file_name, include_ts) do
{:ok, output_path} ->
case File.copy(path_to_monitor, output_path) do
{:ok, bytes} ->
case File.exists?(output_path) do
true ->
config = log_step_result(config, :file_copied, :pass)
Logger.info("Log copied: #{output_path} size: #{bytes}")
config =
case File.write(path_to_monitor, "", [:write]) do
:ok ->
log_step_result(config, :file_copied, :pass)
{:error, _reason} ->
log_step_result(config, :file_copied, :err)
end
config =
if compression do
case compress_file(output_path, storage_file_name) do
:ok ->
log_step_result(config, :file_compressed, :pass)
{:error, reason} ->
Logger.error(
"failed to compress file: #{storage_file_name} due to error: #{reason}"
)
log_step_result(config, :file_compressed, :err)
end
end
config
false ->
Logger.error("Failed to locate file: #{output_path}")
log_step_result(config, :backup_log, :err)
end
{:error, error} ->
Logger.error("failed to copy file: #{path_to_monitor} due to error: #{error}")
log_step_result(config, :file_copied, :pass)
end
end
_ ->
log_step_result(config, :backup_log, :noop)
end
end
config
end
def compress_file(file_path, compressed_file_name) do
case File.read(file_path) do
{:ok, content} ->
compressed_content = :zlib.gzip(content)
case File.write(compressed_file_name, compressed_content) do
:ok ->
:ok
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
defp check_backup_count(
%{
max_storage_count: max_storage_count,
storage_path: storage_path,
storage_file_name: storage_file_name
} = config,
first_pass
) do
with {:ok, files} <- File.ls(storage_path) do
files_with_meta =
files
|> Enum.filter(&String.starts_with?(&1, storage_file_name))
|> Enum.map(&file_info(storage_path, &1))
case Enum.count(files_with_meta) do
x when x > max_storage_count ->
config =
if first_pass do
config
|> log_step_result(:backup_count, x)
|> remove_excess_backup(files_with_meta)
|> check_backup_count(false)
else
config
|> remove_excess_backup(files_with_meta)
|> check_backup_count(false)
end
log_step_result(config, :backups, :pass)
x when x <= max_storage_count ->
Logger.info("#{x} <= #{max_storage_count}")
log_step_result(config, :backups, :pass)
_ ->
Logger.info("What just happened?")
log_step_result(config, :backups, :err)
end
else
{:error, _reason} ->
log_step_result(config, :backups, :err)
end
end
defp remove_excess_backup(%{storage_path: storage_path} = config, files_with_meta) do
[file | _] = Enum.sort_by(files_with_meta, & &1.modified_at, {:asc, Date})
config =
case File.rm(storage_path <> "/" <> file.name) do
:ok -> log_step_result(config, :remove_file, :pass)
{:error, _reason} -> log_step_result(config, :remove_file, :err)
end
config
end
defp file_info(directory, filename) do
full_path = Path.join(directory, filename)
{:ok, stat} = File.stat(full_path, time: :posix)
%{
name: filename,
created_at: stat.ctime |> posix_to_naive_datetime(),
modified_at: stat.mtime |> posix_to_naive_datetime()
}
end
defp posix_to_naive_datetime(posix_time) do
posix_time
|> DateTime.from_unix!()
|> DateTime.to_naive()
end
defp log_step_result(config, name, state) do
Map.update(config, :validation, %{:name => state}, fn x -> Map.put(x, name, state) end)
end
end