Last active
May 30, 2022 08:55
-
-
Save timsu/9d23ccda7e05495f764a9b13a9f6f635 to your computer and use it in GitHub Desktop.
LogDNA Logger for Elixir
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Usage: | |
# | |
# Add worker(LogDNA.BatchLogger, []) to your application supervision tree. | |
# Add Mojito to your mix deps (or replace with HTTPoison or another HTTP library) | |
# | |
# Call LogDNA.BatchLogger.{debug / info / warn / error} to send logs to LogDNA | |
defmodule LogDNA do | |
require Logger | |
@url "https://logs.logdna.com/logs/ingest" | |
@tags "elixir" | |
@app "your_app" | |
@key Application.get_env(:your_app, :logdna_key) | |
defmodule LogDNA.Line do | |
defstruct [ | |
:line, | |
:app, | |
:level, | |
:env, | |
:meta, | |
:timestamp | |
] | |
end | |
def ignored(nil), do: true | |
def ignored(line) do | |
msg = line.line | |
String.contains?(msg, "params=%{} status=200") | |
end | |
@spec post([LogDNA.Line], binary, binary, binary, binary) :: :ok | :error | :nop | |
def post(lines, hostname, mac \\ nil, ip \\ nil, tags \\ nil) do | |
lines = Enum.filter(lines, &(!ignored(&1))) | |
if !@key or length(lines) == 0 do | |
:nop | |
else | |
post_body = %{ | |
lines: lines | |
} | |
body = Poison.encode!(post_body) | |
headers = [{"Content-Type", "application/json"}, {"charset", "UTF-8"}, {"apikey", @key}] | |
now = :os.system_time(:millisecond) | |
ip = if String.contains?(ip, "."), do: ip, else: nil | |
params = [hostname: hostname, mac: mac, ip: ip, now: now, tags: tags] | |
|> Enum.filter(fn {_k, v} -> v end) | |
|> URI.encode_query | |
case Mojito.post("#{@url}?#{params}", headers, body) do | |
{:ok, %Mojito.Response{status_code: 200}} -> :ok | |
{:ok, e} -> | |
IO.inspect(e, label: "LogDNA 200 Error") | |
:error | |
{:error, %Mojito.Error{message: message, reason: reason}} -> | |
message = message || inspect(reason) | |
IO.inspect(message, label: "LogDNA Non-200 Error") | |
:error | |
other -> | |
IO.inspect(other, label: "LogDNA Other Error") | |
:error | |
end | |
end | |
end | |
@spec post([LogDNA.Line]) :: :ok | :error | :nop | |
def post(lines) do | |
hostname = :inet.gethostname |> elem(1) |> to_string | |
ip = Node.self |> to_string | |
post(lines, hostname, nil, ip, @tags) | |
end | |
@spec line(binary, map, binary, integer) :: LogDNA.Line | |
def line(line, meta \\ nil, level \\ "INFO", timestamp \\ :os.system_time(:millisecond)) do | |
%LogDNA.Line{ | |
line: line, | |
app: @app, | |
level: level, | |
meta: meta, | |
timestamp: timestamp | |
} | |
end | |
end | |
defmodule LogDNA.BatchLogger do | |
@name __MODULE__ | |
@timeout 5_000 | |
@size 100 | |
use YourApp.BatchProcessor | |
def debug(message, meta \\ nil), do: log("DEBUG", message, meta) | |
def info(message, meta \\ nil), do: log("INFO", message, meta) | |
def warn(message, meta \\ nil), do: log("WARN", message, meta) | |
def error(message, meta \\ nil), do: log("ERROR", message, meta) | |
def log(level, message, meta \\ nil) do | |
request(LogDNA.line(message, meta, level)) | |
end | |
defp send_api(requests) do | |
try do | |
LogDNA.post(requests) | |
rescue | |
e -> IO.inspect(e, label: "LogDNA try/rescue Error") | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# BatchProcessor | |
# processes requests in batch | |
# you must define @timeout, @size, and a send_api(requests) function | |
# inspired by: https://stackoverflow.com/questions/52570520/how-create-batch-process-in-requests-elixir-phoenix | |
# defmodule ExampleProcessor do | |
# @timeout 5_000 | |
# @size 10 | |
# use YourApp.BatchProcessor | |
# | |
# defp send_api(requests) do | |
# IO.puts "sending #{length requests} requests" | |
# end | |
# end | |
defmodule YourApp.BatchProcessor do | |
defmacro __using__(_opts) do | |
quote do | |
use GenServer | |
@name __MODULE__ | |
def start_link(args \\ []) do | |
GenServer.start_link(__MODULE__, args, name: @name) | |
end | |
def request(req) do | |
GenServer.cast(@name, {:request, req}) | |
end | |
def init(_) do | |
{:ok, %{timer_ref: nil, requests: []}} | |
end | |
def handle_cast({:request, req}, state) do | |
{:noreply, state |> update_in([:requests], & [req | &1]) |> handle_request()} | |
end | |
def handle_info(:timeout, state) do | |
# sent to another API | |
send_api(state.requests) | |
{:noreply, reset_requests(state)} | |
end | |
def handle_info(_, state) do | |
# ignore | |
{:noreply, state} | |
end | |
defp handle_request(%{requests: requests} = state) when length(requests) == 1 do | |
start_timer(state) | |
end | |
defp handle_request(%{requests: requests} = state) when length(requests) > @size do | |
# sent to another API | |
send_api(requests) | |
reset_requests(state) | |
end | |
defp handle_request(state) do | |
state | |
end | |
defp reset_requests(state) do | |
state | |
|> Map.put(:requests, []) | |
|> cancel_timer() | |
end | |
defp start_timer(state) do | |
timer_ref = Process.send_after(self(), :timeout, @timeout) | |
state | |
|> cancel_timer() | |
|> Map.put(:timer_ref, timer_ref) | |
end | |
defp cancel_timer(%{timer_ref: nil} = state) do | |
state | |
end | |
defp cancel_timer(%{timer_ref: timer_ref} = state) do | |
Process.cancel_timer(timer_ref) | |
Map.put(state, :timer_ref, nil) | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment