Skip to content

Instantly share code, notes, and snippets.

@timsu
Last active May 30, 2022 08:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timsu/9d23ccda7e05495f764a9b13a9f6f635 to your computer and use it in GitHub Desktop.
Save timsu/9d23ccda7e05495f764a9b13a9f6f635 to your computer and use it in GitHub Desktop.
LogDNA Logger for Elixir
# 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
# 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