# `Otel.Logs.LogRecordStorage`
[🔗](https://github.com/yangbancode/otel/blob/main/lib/otel/logs/log_record_storage.ex#L1)

ETS-backed FIFO queue for log records awaiting export.

Each row is `{key, %Otel.Logs.LogRecord{}, inserted_at_ms}`
where the key is `:erlang.unique_integer([:positive])` — a
positive integer guaranteed to be unique within the BEAM
lifetime — and `inserted_at_ms` is the millisecond timestamp
stamped at `insert/1` time, used solely by the sweep loop.
The underlying table is `:set` (hash-based), matching
`Otel.Trace.SpanStorage`'s shape and giving O(1) insert in the
hot emit path. Take order is hash-iteration order (not emission
order); this is fine because each log record carries its own
`timestamp` / `observed_timestamp` and the collector orders by
those.

Logs are atomic events — there is no `:active` / `:completed`
lifecycle like spans. Records enter via `insert/1` and leave
via `take/1` when the exporter drains them.

## Public API

| Function | Role |
|---|---|
| `insert/1` | enqueue a log record (back-pressure aware) |
| `take/1` | take + delete a batch of oldest records (Exporter only) |

Mutation flow used by `Otel.Logs.Logger`:

    Otel.Logs.LogRecordStorage.insert(record)

No `get` or `update` — log records are immutable once enqueued.

## Concurrency

Multi-writer + single-reader (the Exporter):

- `insert/1` runs on the caller process and writes to ETS
  directly (`write_concurrency` makes this lock-free).
- `take/1` is called only by `Otel.Logs.LogRecordExporter`
  (single reader — no take/insert races).

## Backpressure

`insert/1` silently drops the record when the ETS table is
already at `@max_queue_size`, matching the spec's
`maxQueueSize` parameter for the Batching processor
(`logs/sdk.md` L540-L541 *"After the size is reached logs are
dropped"*). Drop is a normal lifecycle event, not a failure
— callers don't branch on the result.

## Sweep — stale records

The GenServer runs a self-scheduled sweep every
`@sweep_interval_ms` (10 minutes) that issues a single
`:ets.select_delete/2` removing rows whose `inserted_at_ms`
(row position 3) is older than `@log_record_ttl_ms` (30
minutes). This is the safety net for records that never get
drained — if `Otel.Logs.LogRecordExporter` is dead or hung,
records would otherwise pile up until the `@max_queue_size`
backpressure starts dropping fresh records. With the sweep,
the oldest stale records age out instead so fresh inserts
keep landing.

Defaults match `Otel.Trace.SpanStorage`'s sweeper for symmetry.
Sweep strategy is `drop` only — exporting half-aged records
serves no spec purpose.

## References

- OTel Logs SDK §LogRecordProcessor: `opentelemetry-specification/specification/logs/sdk.md` L468-L545
- Erlang `:erlang.unique_integer/1`: <https://www.erlang.org/doc/man/erlang#unique_integer-1>

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `insert`

```elixir
@spec insert(log_record :: Otel.Logs.LogRecord.t()) :: :ok
```

Enqueue a log record. Always returns `:ok` — silent drop when
the table is at `@max_queue_size` (spec `logs/sdk.md` L540-L541
*"After the size is reached logs are dropped"*: drop is a
normal lifecycle event, not a failure).

# `start_link`

```elixir
@spec start_link(opts :: keyword()) :: GenServer.on_start()
```

# `take`

```elixir
@spec take(n :: pos_integer()) :: [Otel.Logs.LogRecord.t()]
```

Take up to `n` log records, deleting them from the table.
Records come back in hash-iteration order (not emission
order). Called only by `Otel.Logs.LogRecordExporter` (single
reader).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
