• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ausimian / loki_logger_handler / 14d0e35c5adf80cfb63e3993037ee11b1988ce7f

22 Jan 2026 11:13PM UTC coverage: 94.059% (+0.2%) from 93.836%
14d0e35c5adf80cfb63e3993037ee11b1988ce7f

push

github

ausimian
Version 0.2.0

285 of 303 relevant lines covered (94.06%)

28.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.75
/lib/loki_logger_handler.ex
1
defmodule LokiLoggerHandler do
2
  @moduledoc """
3
  Elixir Logger handler for Grafana Loki.
4

5
  This library implements an Erlang `:logger` handler that buffers logs and sends
6
  them to Loki in batches. It supports:
7

8
  - Configurable label extraction for Loki stream labels
9
  - Structured metadata (Loki 2.9+)
10
  - Two storage strategies: disk (CubDB) or memory (ETS)
11
  - Dual threshold batching (time and size)
12
  - Exponential backoff on failures
13
  - Multiple handler instances for different Loki endpoints
14

15
  ## Quick Start
16

17
      # Attach a handler
18
      LokiLoggerHandler.attach(:my_handler,
19
        loki_url: "http://localhost:3100",
20
        labels: %{
21
          app: {:static, "myapp"},
22
          env: {:metadata, :env},
23
          level: :level
24
        },
25
        structured_metadata: [:request_id, :user_id]
26
      )
27

28
      # Now use Logger as usual
29
      require Logger
30
      Logger.info("Hello Loki!", request_id: "abc123")
31

32
      # Later, detach if needed
33
      LokiLoggerHandler.detach(:my_handler)
34

35
  ## Configuration Options
36

37
  | Option | Type | Default | Description |
38
  |--------|------|---------|-------------|
39
  | `:loki_url` | string | required | Loki push API base URL |
40
  | `:storage` | atom | `:disk` | Storage strategy: `:disk` (CubDB) or `:memory` (ETS) |
41
  | `:labels` | map | `%{level: :level}` | Label extraction config |
42
  | `:structured_metadata` | list | `[]` | Metadata keys for Loki structured metadata |
43
  | `:data_dir` | string | `"priv/loki_buffer/<id>"` | CubDB storage directory (disk only) |
44
  | `:batch_size` | integer | 100 | Max entries per batch |
45
  | `:batch_interval_ms` | integer | 5000 | Max time between batches |
46
  | `:max_buffer_size` | integer | 10000 | Max buffered entries before dropping |
47
  | `:backoff_base_ms` | integer | 1000 | Base backoff on failure |
48
  | `:backoff_max_ms` | integer | 60000 | Max backoff time |
49

50
  ## Label Configuration
51

52
  Labels are configured as a map where keys are the Loki label names and values
53
  specify how to extract the label value:
54

55
  - `:level` - Use the log level
56
  - `{:metadata, key}` - Extract from log metadata
57
  - `{:static, value}` - Use a static value
58

59
  Example:
60

61
      labels: %{
62
        app: {:static, "myapp"},
63
        environment: {:metadata, :env},
64
        level: :level,
65
        node: {:metadata, :node}
66
      }
67

68
  ## Structured Metadata (Loki 2.9+)
69

70
  Structured metadata allows attaching key-value pairs that aren't indexed as labels
71
  but can still be queried. Specify a list of metadata keys to extract:
72

73
      structured_metadata: [:request_id, :user_id, :trace_id, :span_id]
74

75
  ## Telemetry
76

77
  The library emits telemetry events for monitoring buffer state:
78

79
  - `[:loki_logger_handler, :buffer, :insert]` - After a log entry is buffered
80
  - `[:loki_logger_handler, :buffer, :remove]` - After entries are sent and removed
81

82
  Both events include:
83
  - Measurements: `%{count: integer}` - Buffer size after the operation
84
  - Metadata: `%{handler_id: atom, storage: :cub | :ets}`
85

86
  Example:
87

88
      :telemetry.attach(
89
        "loki-buffer-monitor",
90
        [:loki_logger_handler, :buffer, :insert],
91
        fn _event, %{count: count}, %{handler_id: id}, _config ->
92
          IO.puts("Handler \#{id} buffer size: \#{count}")
93
        end,
94
        nil
95
      )
96

97
  """
98

99
  alias LokiLoggerHandler.Handler
100

101
  @type handler_id :: atom()
102
  @type option ::
103
          {:loki_url, String.t()}
104
          | {:storage, :disk | :memory}
105
          | {:labels, map()}
106
          | {:structured_metadata, [atom()]}
107
          | {:data_dir, String.t()}
108
          | {:batch_size, pos_integer()}
109
          | {:batch_interval_ms, pos_integer()}
110
          | {:max_buffer_size, pos_integer()}
111
          | {:backoff_base_ms, pos_integer()}
112
          | {:backoff_max_ms, pos_integer()}
113

114
  @doc """
115
  Attaches a new Loki logger handler.
116

117
  ## Parameters
118
    * `handler_id` - A unique atom identifier for this handler
119
    * `opts` - Configuration options (see module docs)
120

121
  ## Returns
122
    * `:ok` on success
123
    * `{:error, reason}` on failure
124

125
  ## Examples
126

127
      LokiLoggerHandler.attach(:default,
128
        loki_url: "http://localhost:3100",
129
        labels: %{app: {:static, "myapp"}, level: :level}
130
      )
131

132
  """
133
  @spec attach(handler_id(), [option()]) :: :ok | {:error, term()}
134
  def attach(handler_id, opts) when is_atom(handler_id) and is_list(opts) do
135
    config = Map.new(opts)
27✔
136

137
    case :logger.add_handler(handler_id, Handler, %{config: config}) do
27✔
138
      :ok -> :ok
22✔
139
      {:error, reason} -> {:error, reason}
5✔
140
    end
141
  end
142

143
  @doc """
144
  Detaches a Loki logger handler.
145

146
  ## Parameters
147
    * `handler_id` - The handler identifier used when attaching
148

149
  ## Returns
150
    * `:ok` on success
151
    * `{:error, reason}` if the handler doesn't exist
152

153
  ## Examples
154

155
      LokiLoggerHandler.detach(:default)
156

157
  """
158
  @spec detach(handler_id()) :: :ok | {:error, term()}
159
  def detach(handler_id) when is_atom(handler_id) do
160
    case :logger.remove_handler(handler_id) do
23✔
161
      :ok -> :ok
22✔
162
      {:error, reason} -> {:error, reason}
1✔
163
    end
164
  end
165

166
  @doc """
167
  Forces an immediate flush of all pending logs for a handler.
168

169
  Useful before application shutdown to ensure all logs are sent.
170

171
  ## Parameters
172
    * `handler_id` - The handler identifier
173

174
  ## Returns
175
    * `:ok` on success
176
    * `{:error, reason}` on failure
177
  """
178
  @spec flush(handler_id()) :: :ok | {:error, term()}
179
  def flush(handler_id) when is_atom(handler_id) do
180
    sender = LokiLoggerHandler.Application.via(LokiLoggerHandler.Sender, handler_id)
2✔
181
    LokiLoggerHandler.Sender.flush(sender)
2✔
182
  end
183

184
  @doc """
185
  Updates the configuration of an existing handler.
186

187
  ## Parameters
188
    * `handler_id` - The handler identifier
189
    * `opts` - New configuration options to merge
190

191
  ## Returns
192
    * `:ok` on success
193
    * `{:error, reason}` on failure
194
  """
195
  @spec update_config(handler_id(), [option()]) :: :ok | {:error, term()}
196
  def update_config(handler_id, opts) when is_atom(handler_id) and is_list(opts) do
197
    config = Map.new(opts)
3✔
198
    :logger.update_handler_config(handler_id, :config, config)
3✔
199
  end
200

201
  @doc """
202
  Returns the current configuration for a handler.
203

204
  ## Parameters
205
    * `handler_id` - The handler identifier
206

207
  ## Returns
208
    * `{:ok, config}` with the handler configuration
209
    * `{:error, reason}` if the handler doesn't exist
210
  """
211
  @spec get_config(handler_id()) :: {:ok, map()} | {:error, term()}
212
  def get_config(handler_id) when is_atom(handler_id) do
213
    case :logger.get_handler_config(handler_id) do
5✔
214
      {:ok, %{config: config}} -> {:ok, config}
5✔
215
      {:error, reason} -> {:error, reason}
×
216
    end
217
  end
218

219
  @doc """
220
  Lists all attached Loki logger handlers.
221

222
  ## Returns
223
  A list of handler IDs that are using this handler module.
224
  """
225
  @spec list_handlers() :: [handler_id()]
226
  def list_handlers do
227
    :logger.get_handler_config()
228
    |> Enum.filter(fn %{module: module} -> module == Handler end)
81✔
229
    |> Enum.map(fn %{id: id} -> id end)
30✔
230
  end
231
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc