README.md

<p align="center">
  <h1 align="center">LogDot SDK for Elixir</h1>
  <p align="center">
    <strong>Cloud logging and metrics made simple</strong>
  </p>
</p>

<p align="center">
  <a href="https://hex.pm/packages/logdot"><img src="https://img.shields.io/hexpm/v/logdot?style=flat-square&color=blue" alt="Hex.pm version"></a>
  <a href="https://hex.pm/packages/logdot"><img src="https://img.shields.io/hexpm/dt/logdot?style=flat-square" alt="Hex.pm downloads"></a>
  <a href="https://github.com/logdot-io/logdot-elixir/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="MIT License"></a>
  <a href="https://elixir-lang.org/"><img src="https://img.shields.io/badge/elixir-%3E%3D1.14-purple?style=flat-square&logo=elixir&logoColor=white" alt="Elixir 1.14+"></a>
  <a href="https://www.erlang.org/"><img src="https://img.shields.io/badge/OTP-%3E%3D25-red?style=flat-square" alt="OTP 25+"></a>
</p>

<p align="center">
  <a href="https://logdot.io">Website</a> •
  <a href="https://docs.logdot.io">Documentation</a> •
  <a href="#quick-start">Quick Start</a> •
  <a href="#api-reference">API Reference</a>
</p>

---

## Features

- **Separate Clients** — Independent Logger and Metrics GenServers for maximum flexibility
- **Context-Aware Logging** — Create loggers with persistent context that automatically flows through your application
- **OTP Compliant** — Built as proper OTP application with supervision tree
- **Entity-Based Metrics** — Create/find entities, then bind to them for organized metric collection
- **Batch Operations** — Efficiently send multiple logs or metrics in a single request
- **Automatic Retry** — Exponential backoff retry with configurable attempts

## Installation

Add `logdot` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:logdot, "~> 1.0"}
  ]
end
```

## Configuration

```elixir
# config/config.exs
config :logdot,
  api_key: "ilog_live_YOUR_API_KEY",
  hostname: "my-app",
  timeout: 5000,
  retry_attempts: 3,
  debug: false
```

## Quick Start

```elixir
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# LOGGING
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LogDot.info("Application started", %{version: "1.0.0"})
LogDot.error("Something went wrong", %{error_code: 500})

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# METRICS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Create or find an entity first
{:ok, entity} = LogDot.Metrics.get_or_create_entity(
  "my-service",
  "My production service",
  %{environment: "production"}
)

# Bind to the entity for sending metrics
:ok = LogDot.Metrics.for_entity(entity.id)

# Now send metrics
LogDot.metric("response_time", 123.45, "ms", %{endpoint: "/api/users"})
```

## Logging

### Log Levels

```elixir
LogDot.debug("Debug message")
LogDot.info("Info message")
LogDot.warn("Warning message")
LogDot.error("Error message")
```

### Structured Tags

```elixir
LogDot.info("User logged in", %{
  user_id: 12345,
  ip_address: "192.168.1.1",
  session_id: "abc123"
})
```

### Context-Aware Logging

Create loggers with persistent context that automatically flows through your application:

```elixir
# Create a context for a specific request
ctx = LogDot.with_context(%{request_id: "abc-123", user_id: 456})

# All logs include request_id and user_id automatically
LogDot.info(ctx, "Processing request", %{})
LogDot.debug(ctx, "Fetching user data", %{})

# Chain contexts — they merge together
detailed_ctx = LogDot.with_context(ctx, %{operation: "checkout"})

# This log has request_id, user_id, AND operation
LogDot.info(detailed_ctx, "Starting checkout process", %{})
```

### Batch Logging

Send multiple logs in a single HTTP request:

```elixir
LogDot.begin_batch()

LogDot.info("Step 1 complete")
LogDot.info("Step 2 complete")
LogDot.info("Step 3 complete")

LogDot.send_batch()  # Single HTTP request
LogDot.end_batch()
```

## Metrics

### Entity Management

```elixir
# Create a new entity
{:ok, entity} = LogDot.Metrics.create_entity(
  "my-service",
  "Production API server",
  %{environment: "production", region: "us-east-1"}
)

# Find existing entity
{:ok, existing} = LogDot.Metrics.get_entity_by_name("my-service")

# Get or create (recommended)
{:ok, entity} = LogDot.Metrics.get_or_create_entity(
  "my-service",
  "Created if not exists"
)
```

### Sending Metrics

```elixir
:ok = LogDot.Metrics.for_entity(entity.id)

# Single metric
LogDot.metric("cpu_usage", 45.2, "percent")
LogDot.metric("response_time", 123.45, "ms", %{
  endpoint: "/api/users",
  method: "GET"
})
```

### Batch Metrics

```elixir
# Same metric, multiple values
LogDot.begin_metric_batch("temperature", "celsius")
LogDot.add_metric(23.5, %{sensor: "room1"})
LogDot.add_metric(24.1, %{sensor: "room2"})
LogDot.add_metric(23.8, %{sensor: "room3"})
LogDot.send_metric_batch()
LogDot.end_metric_batch()

# Multiple different metrics
LogDot.begin_multi_batch()
LogDot.add_multi_metric("cpu", 45.5, "percent")
LogDot.add_multi_metric("memory", 8192, "MB")
LogDot.add_multi_metric("disk", 75.0, "percent")
LogDot.send_metric_batch()
LogDot.end_metric_batch()
```

## Auto-Instrumentation (Phoenix)

Automatically capture Phoenix HTTP requests, errors, and Ecto database queries using
the built-in `:telemetry` system. No additional dependencies required.

### Setup

In your `application.ex`:

```elixir
def start(_type, _args) do
  # Initialize metrics entity (required for duration metrics)
  LogDot.init_metrics()

  LogDot.Phoenix.attach(
    ecto_prefixes: [[:my_app, :repo]]
  )

  children = [
    # ... your supervision tree
  ]

  Supervisor.start_link(children, strategy: :one_for_one)
end
```

### What Gets Captured

- **Phoenix requests** — Every endpoint request with method, path, status, and duration
- **Phoenix errors** — Errors with kind, reason, and stacktrace
- **Ecto queries** — Database queries with source table, query time, and queue time
- **Metrics** — Request and query duration (requires `LogDot.init_metrics()` to be called first, which automatically creates/resolves the entity using the configured `:entity_name`)

### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `:endpoint` | boolean | `true` | Log Phoenix HTTP requests |
| `:errors` | boolean | `true` | Log Phoenix errors |
| `:ecto` | boolean | `true` | Log Ecto database queries |
| `:metrics` | boolean | `true` | Send duration metrics |
| `:ignore_paths` | list | `[]` | Paths to skip (e.g. `["/health"]`) |
| `:ecto_prefixes` | list | `[]` | Ecto telemetry prefixes for your repos |
| `:capture_logging` | boolean | `false` | Forward Elixir `Logger` calls to LogDot |

### Detaching

```elixir
LogDot.Phoenix.detach()
```

## Log Capture

Automatically forward Elixir `Logger` calls to LogDot. The original Logger behavior is preserved — messages still appear in your console and file backends as usual.

This works in **any Elixir/OTP application** (Plug, Bandit, standalone GenServers, Mix tasks, etc.), not just Phoenix. If you use `LogDot.Phoenix.attach(capture_logging: true)`, the handler is installed automatically — don't also call `LogDot.capture_logging()` or it will return `{:error, ...}` since the handler is already registered.

### Standalone Usage

```elixir
# Start capturing
LogDot.capture_logging()

# All Logger calls are now sent to LogDot
require Logger
Logger.info("This goes to LogDot")          # severity: info
Logger.error("Something failed")             # severity: error
Logger.warning("Watch out")                  # severity: warn
Logger.debug("Debug details")                # severity: debug

# Stop capturing
LogDot.stop_capture_logging()
```

### With Phoenix

When using Phoenix auto-instrumentation, pass `capture_logging: true`:

```elixir
# application.ex
def start(_type, _args) do
  LogDot.Phoenix.attach(
    capture_logging: true,
    ecto_prefixes: [[:my_app, :repo]]
  )

  children = [...]
  Supervisor.start_link(children, strategy: :one_for_one)
end
```

### How It Works

`LogDot.LoggerBackend` is an Erlang `:logger` handler:

1. Installed via `:logger.add_handler/3` (OTP 21+ logging system)
2. Receives all log events from the BEAM — `Logger.info/1`, `Logger.error/1`, `IO.puts/1` (when `:console` backend is not the only handler), etc.
3. Formats the message (supports `:string`, `:report`, and `:io_lib.format` message formats)
4. Maps the Erlang/Elixir log level to a LogDot severity
5. Sends via `LogDot.log/3` (the existing GenServer-backed API)
6. A **process dictionary guard** (`Process.get(:logdot_sending)`) prevents infinite loops — when LogDot's own HTTP client triggers Logger events during delivery, those events are silently skipped
7. Messages longer than 16KB are truncated
8. All errors are rescued — a crashed `:telemetry` handler gets permanently detached, but a crashed `:logger` handler can also cause issues, so defensive coding is essential

### Level Mapping

| Erlang/Elixir Level | LogDot Severity |
|---------------------|----------------|
| `:emergency` | `error` |
| `:alert` | `error` |
| `:critical` | `error` |
| `:error` | `error` |
| `:warning` | `warn` |
| `:warn` | `warn` |
| `:notice` | `info` |
| `:info` | `info` |
| `:debug` | `debug` |

### Tags

All captured Logger logs include `{ source: "elixir_logger" }` in their tags, so you can filter them from manually sent logs in the LogDot dashboard.

### Manual Handler Management

For advanced usage, you can manage the handler directly:

```elixir
# Add handler manually
:logger.add_handler(:logdot_backend, LogDot.LoggerBackend, %{})

# Remove handler manually
:logger.remove_handler(:logdot_backend)
```

## API Reference

### LogDot (Main Module)

| Function | Description |
|----------|-------------|
| `with_context(context)` | Create a context struct |
| `with_context(ctx, context)` | Chain contexts together |
| `get_context(ctx)` | Get context map |
| `debug/info/warn/error(message, tags \\ %{})` | Send log at level |
| `debug/info/warn/error(ctx, message, tags)` | Send log with context |
| `begin_batch()` | Start batch mode |
| `send_batch()` | Send queued logs |
| `end_batch()` | End batch mode |
| `clear_batch()` | Clear queue without sending |
| `capture_logging()` | Start forwarding Logger calls to LogDot |
| `stop_capture_logging()` | Stop forwarding Logger calls |

### LogDot.Metrics

| Function | Description |
|----------|-------------|
| `create_entity(name, description, metadata)` | Create new entity |
| `get_entity_by_name(name)` | Find entity by name |
| `get_or_create_entity(name, description, metadata)` | Get or create entity |
| `for_entity(entity_id)` | Bind to entity for metrics |

## Requirements

- Elixir 1.14+
- OTP 25+
- [Req](https://hex.pm/packages/req) HTTP client

## Examples

Create a `.env` file in the repo root with your API key:

```
LOGDOT_API_KEY=ilog_live_YOUR_API_KEY
```

### Core SDK test app

Tests logging, metrics, context, and batch operations:

```bash
cd elixir
mix run --no-start examples/test_app.exs
```

### Hooks test app (Phoenix + Logger Backend)

Tests Phoenix telemetry handlers and logger backend capture:

```bash
cd elixir
mix run --no-start examples/test_hooks.exs
```

## License

MIT License — see [LICENSE](LICENSE) for details.

---

<p align="center">
  <a href="https://logdot.io">logdot.io</a> •
  Built with care for developers
</p>