# `PhoenixGenApi.Tracer`
[🔗](https://github.com/ohhi-vn/phoenix_gen_api/blob/main/lib/phoenix_gen_api/tracer/tracer.ex#L1)

Request tracing for PhoenixGenApi with per-key log files.

Traces requests that match a configured `request_type` or `user_id`, writing
each trace as a `key=value` line to a dedicated log file (one file per
request type, one file per user id).

## Why this is fast

The hot-path check performed on every executed request is deliberately tiny:

  * a single `:persistent_term` read to know whether tracing is enabled at all
  * two in-memory membership checks against the enabled `request_type` /
    `user_id` sets
  * if nothing matches, the call returns immediately

Only when a request matches does it dispatch an asynchronous message to the
writer process, which performs the file I/O. Tracing therefore never blocks
request execution and adds negligible overhead when disabled or unmatched.

## Enabling tracing

Tracing is fully opt-in. It is disabled by default and only writes when a
request matches an explicitly enabled request type or user id.

### At runtime (functions)

    # Trace a single request type
    PhoenixGenApi.Tracer.enable_request_type("get_user")

    # Trace several request types at once
    PhoenixGenApi.Tracer.enable_request_type(["get_user", "create_order"])

    # Trace all requests for a specific user
    PhoenixGenApi.Tracer.enable_user_id("user_123")

    # Stop tracing
    PhoenixGenApi.Tracer.disable_request_type("get_user")
    PhoenixGenApi.Tracer.disable_user_id("user_123")

### Via configuration

    config :phoenix_gen_api, :tracer,
      enabled: true,
      log_dir: "log/phoenix_gen_api_traces",
      max_file_bytes: 50_000_000,
      max_backup_files: 5,
      log_level: :debug,
      request_types: ["get_user"],
      user_ids: ["user_123"]

Config options:

  * `:enabled` — global on/off switch (default: `false`)
  * `:log_dir` — directory for the per-key trace files (default:
    `"log/phoenix_gen_api_traces"`)
  * `:max_file_bytes` — rotate a trace file once it exceeds this size
    (default: `50_000_000` = 50 MB)
  * `:max_backup_files` — how many rotated `.1`, `.2`, ... backup files to
    keep (default: `5`)
  * `:log_level` — `Logger` level temporarily raised globally while tracing
    is enabled, so debug/info output is captured (default: `:debug`)
  * `:request_types` — request types to trace at startup
  * `:user_ids` — user ids to trace at startup

## Trace files

Files are named after the traced key:

    log/phoenix_gen_api_traces/request_type-get_user.log
    log/phoenix_gen_api_traces/user_id-user_123.log

When a request matches both an enabled request type and an enabled user id,
one line is written to each matching file.

Each line is space-separated `key=value`, e.g.:

    timestamp=2026-08-17T12:00:00.000Z node=app@host event=request_start     request_id=req_1 user_id=user_123 device_id=dev_1 request_type=get_user     service=user_service version=nil args="%{"id" => "u1"}"

## Tracing every action of a request

Tracing does not stop at the three lifecycle events. While a request is being
traced, the trace file records every action the request takes:

  * structured milestone events (config lookup, hooks, rate limit, permission,
    argument conversion, execution, retries, RPC fallback, async/stream
    dispatch, errors)
  * raw `Logger` output emitted by any process that touches the request
    (`event=log` lines with `level`, `pid`, `mfa` and `message`)

Raw log capture is driven by process metadata: `begin_trace/1` attaches
`phoenix_gen_api_trace` and `phoenix_gen_api_trace_request` metadata to the
current process, and worker processes re-apply that metadata so their logs are
captured too. While tracing is enabled the global `Logger` level is raised to
`:log_level` so debug/info messages reach the capture handler, and it is
restored when tracing is disabled.

The trace context can also be managed by hand:

    ctx = PhoenixGenApi.Tracer.begin_trace(request)  # nil when not traced
    PhoenixGenApi.Tracer.trace_event("my_step", %{"status" => "ok"})
    PhoenixGenApi.Tracer.end_trace(ctx)              # restores metadata

Events written per traced request:

  * `event=request_start` — emitted when the executor begins handling the
    request, includes the full request (including `args`)
  * `event=config_lookup` — the service config was resolved (`ok`), missing
    (`not_found`) or disabled
  * `event=hook_before` — a `before_execute` hook ran (`ok`) or failed
  * `event=rate_limit` — rate limiter decision: `allowed`, `limited`
    (with `retry_after_ms`) or `error`
  * `event=permission` — emitted after the permission check, includes
    `permission=allowed|denied` and `permission_mode`
  * `event=arguments` — argument conversion succeeded (with `count`) or failed
  * `event=execution` — the MFA was invoked, with `mode=local|remote` and `mfa`
  * `event=error` — execution raised/exited/errored, with `kind` and `error`
  * `event=retry` / `event=retry_exhausted` — local and remote retry attempts
  * `event=rpc_fallback` — a remote node failed and the request fell back
  * `event=async` / `event=stream` — async/stream dispatch (`queued`,
    `queue_full`, `started`, `timeout`, `error`)
  * `event=hook_after` — an `after_execute` hook ran (`ok`) or failed
  * `event=log` — a raw `Logger` line emitted during the trace
  * `event=request_end` — emitted when execution finishes, includes
    `success`, `async`, `duration_us` and `error` (if any)

## Inspecting state

    PhoenixGenApi.Tracer.enabled?()
    PhoenixGenApi.Tracer.enabled_request_types()
    PhoenixGenApi.Tracer.enabled_user_ids()
    PhoenixGenApi.Tracer.status()

# `apply_trace_metadata`

```elixir
@spec apply_trace_metadata(PhoenixGenApi.Structs.Request.t()) :: :ok
```

Attaches trace metadata to the current process when the given request is
traced.

Used by processes that continue request execution in a separate process
(e.g. `PhoenixGenApi.StreamCall`), so their `Logger` output is captured.

# `begin_trace`

```elixir
@spec begin_trace(PhoenixGenApi.Structs.Request.t()) ::
  {keyword(), [{atom(), String.t()}]} | nil
```

Starts a trace context for a request in the current process.

When the request matches an enabled request type or user id, this writes the
`request_start` line and attaches trace metadata to the current process so
that both `trace_event/2` calls and raw `Logger` output emitted while the
request is processed are captured into the matching trace files.

Returns `nil` when the request is not traced; otherwise a context that must
be passed to `end_trace/1` to restore the process metadata.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear`

```elixir
@spec clear() :: :ok
```

Clears all traced request types and user ids.

# `configure`

```elixir
@spec configure(keyword()) :: :ok | {:error, :not_started}
```

Updates writer settings at runtime.

Accepts a keyword list with any of `:log_dir`, `:max_file_bytes`, or
`:max_backup_files`. Open trace files are not relocated; the new settings
apply to subsequently opened files.

# `disable_request_type`

```elixir
@spec disable_request_type(String.t() | [String.t()]) :: :ok
```

Disables tracing for one or more request types.

# `disable_user_id`

```elixir
@spec disable_user_id(String.t() | [String.t()]) :: :ok
```

Disables tracing for one or more user ids.

# `enable_request_type`

```elixir
@spec enable_request_type(String.t() | [String.t()]) :: :ok
```

Enables tracing for one or more request types.

Accepts a single binary or a list of binaries.

# `enable_user_id`

```elixir
@spec enable_user_id(String.t() | [String.t()]) :: :ok
```

Enables tracing for one or more user ids.

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Returns `true` when tracing is globally enabled.

Note: tracing is only written for requests that also match an enabled
request type or user id.

# `enabled_request_types`

```elixir
@spec enabled_request_types() :: [String.t()]
```

Returns the list of currently traced request types.

# `enabled_user_ids`

```elixir
@spec enabled_user_ids() :: [String.t()]
```

Returns the list of currently traced user ids.

# `end_trace`

```elixir
@spec end_trace({keyword(), term()} | nil) :: :ok
```

Ends a trace context started with `begin_trace/1`, restoring the process
metadata to its previous state.

# `flush`

```elixir
@spec flush() :: :ok | :error
```

Blocks until the writer has processed all pending trace messages.

Intended for tests and administrative use; normal production code should
rely on the asynchronous writer.

# `set_enabled`

```elixir
@spec set_enabled(boolean()) :: :ok
```

Enables or disables tracing globally at runtime.

While tracing is enabled, the primary log level is raised to the configured
`:log_level` (default `:debug`) so that debug/info output from traced
requests can be captured; the previous level is restored when tracing is
disabled. Low-level messages that do not belong to a traced request remain
filtered out of the console.

# `start_link`

Starts the tracer writer process and loads the `:tracer` config.

# `status`

```elixir
@spec status() :: map()
```

Returns a status snapshot including config, enabled keys and open trace files.

# `trace_event`

```elixir
@spec trace_event(String.t(), map()) :: :ok
```

Writes a structured milestone event for the request currently being traced
in the current process.

Requires an active trace context (set by `begin_trace/1` or
`apply_trace_metadata/1`). The event line includes the traced request's
context fields plus `extra` (a string-keyed map of `key => value`).

# `trace_permission`

```elixir
@spec trace_permission(
  PhoenixGenApi.Structs.Request.t(),
  PhoenixGenApi.Structs.FunConfig.t(),
  :allowed | :denied
) :: :ok
```

Traces the permission check result for a request.

`result` is `:allowed` or `:denied`. Includes the permission mode
(e.g. `{:arg, "user_id"}` or `{:callback, {mod, fun, args}}`).

# `trace_request`

```elixir
@spec trace_request(PhoenixGenApi.Structs.Request.t()) :: :ok
```

Hot-path hook called by the executor when a request starts executing.

No-op when tracing is disabled or the request matches nothing. Cheap by
design: two in-memory membership lookups and, only on match, an
asynchronous message to the writer.

# `trace_result`

```elixir
@spec trace_result(PhoenixGenApi.Structs.Request.t(), term(), non_neg_integer()) ::
  :ok
```

Traces the final execution result of a request.

`result` is the value returned by the executor (a `%Response{}`, an
`{:ok, :no_response}` tuple, or an exception-derived error).

---

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