<!--
  Monitoring & Observability
  From the Iris book, chapter 17, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/monitoring-observability/
-->
# Chapter 17: Monitoring & Observability

An application you cannot observe is an application you cannot operate.
Iris ships the observability essentials in-tree: a structured access log,
request correlation IDs, an application logger, a live process dashboard,
the Go profiling endpoints, per-request cost measurement, `Server-Timing`
responses and a health endpoint that reports build information. This
chapter wires each of them the way you would in production, then closes
with an honest look at where third-party tools (Prometheus,
OpenTelemetry) take over.

## Background

The day you deploy, your relationship with your own code changes. On your
machine, when something misbehaves, you attach a debugger, set a
breakpoint and inspect variables at leisure. In production none of that
exists: the process runs on a remote machine, serves real users and
cannot be paused. By the time someone reports a problem, the moment that
caused it is minutes or days gone. The only way to understand it is
through what the application recorded *while it happened*. Observability
is the discipline of emitting that evidence deliberately (before you
need it) instead of hoping to reconstruct events from memory.

The evidence comes in three shapes, and the industry uses three words for
them:

- **Logs** are timestamped records of individual events: "this request
  returned 500 at 03:12:07", "user 42 signed up". They answer *what
  exactly happened*. A *structured* log records each entry as key-value
  data (in practice, one JSON object per line) instead of free-form
  prose, so tools can filter and index it rather than merely display it.
- **Metrics** are numbers tracked over time: requests per second, memory
  in use, the error rate. A metric forgets individual events and keeps
  only the aggregate, which makes it tiny to store and ideal for graphs
  and alerts. Metrics answer *how the system is trending*.
- **Traces** follow one request across every service it touches, with
  timing for each step: this instance, then the database, then a
  downstream API. Traces answer *where the time went* when a single
  operation spans several processes.

A rough rule: metrics tell you something is wrong, logs tell you what,
traces tell you where.

Two more terms carry this chapter. A *health check* is a cheap endpoint
whose only job is to be polled by machines. The poller is typically an
*orchestrator*, supervising software such as a load balancer or
Kubernetes that starts, stops and routes traffic to your instances. A
failing *liveness* check tells the orchestrator "this process is dead,
restart it"; a failing *readiness* check tells it "this instance cannot
serve right now, send traffic elsewhere", a distinction the health
section returns to.

Finally, request IDs. One production request leaves fragments everywhere:
a line in the access log, several in the application log, entries on the
database and on downstream services, all interleaved with the fragments
of thousands of neighboring requests. When a user reports one failure,
finding its fragments by timestamp is archaeology. A *request ID* (or
correlation ID) is a unique value stamped onto every record a request
produces, across every system it touches. Debugging at 3am then starts
with one search: filter all logs by the ID from the user's error message,
and the complete story of that single request reads out in order. Little
in this chapter is glamorous, but this one habit routinely turns hours of
guessing into minutes of reading.

## Structured Access Logging

[Chapter 12](https://iris-go.com/docs/built-in-middleware/) introduced `middleware/accesslog`
as a one-liner. In production the access log is the backbone of your
observability: one record per request (latency, status, sizes, custom
fields) written as JSON lines that any log shipper can tail, parse and
index. The configuration below is tuned for exactly that:

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/accesslog"
    "github.com/kataras/iris/v14/middleware/requestid"
)

func newAccessLog() *accesslog.AccessLog {
    ac := accesslog.File("access_log.json")

    // Fire the log line in a goroutine on a copy of the request
    // context, so the response never waits on log I/O.
    ac.Async = true

    // Keep payloads out of production logs: privacy and volume.
    ac.RequestBody = false
    ac.ResponseBody = false

    // Body sizes are cheap to record. The full wire sizes
    // (BytesReceived/BytesSent) dump the whole request and response
    // to count headers too — expensive, leave them off.
    ac.BytesReceivedBody = true
    ac.BytesSentBody = true

    // One line per record, even when a panic is logged.
    ac.KeepMultiLineError = false
    ac.PanicLog = accesslog.LogHandler

    // Compact JSON: one object per line.
    ac.SetFormatter(&accesslog.JSON{})

    // Attach the correlation ID to every record.
    ac.AddFields(func(ctx iris.Context, f *accesslog.Fields) {
        f.Set("request_id", requestid.Get(ctx))
    })

    return ac
}

func main() {
    app := iris.New()

    ac := newAccessLog()
    defer ac.Close()

    app.UseRouter(ac.Handler)
    app.Use(requestid.New(requestid.ServerOnlyGenerator))

    app.Get("/", func(ctx iris.Context) {
        ctx.JSON(iris.Map{"hello": "world"})
    })

    app.Listen(":8080")
}
```

Register the handler with `UseRouter` so the log also captures requests
that never match a route: 404s, CORS preflights, rejected requests.
`accesslog.File(path)` opens a buffered file (flushed on `Close`),
`accesslog.FileUnbuffered(path)` trades throughput for immediate writes,
and `accesslog.New(w io.Writer)` accepts any writer, the hook for log
rotation below.

### The Record

With the JSON formatter each line carries these keys (empty ones are
omitted): `timestamp` (unix milliseconds), `latency`, `code`, `method`,
`path`, `ip`, `query`, `params`, `fields`, `request`, `response`,
`bytes_received`, `bytes_sent`. The `query`, `params` and `fields` keys
hold arrays of `{"key": ..., "value": ...}` entries, the same shape in
compact and indented output. Without a formatter the default text
format is:

```
Time|Latency|Code|Method|Path|IP|Path Params Query Fields|Bytes Received|Bytes Sent|Request|Response|
```

Three formatters ship with the package, all set through `SetFormatter`:

```go
// JSON lines. A non-empty Indent pretty-prints instead;
// HumanTime replaces the unix timestamp with TimeFormat text.
ac.SetFormatter(&accesslog.JSON{})

// CSV, optionally with a header row on a fresh file.
ac.SetFormatter(&accesslog.CSV{Header: true})

// Full control via text/template.
ac.SetFormatter(&accesslog.Template{
    Text: "{{.Now.Format .TimeFormat}} {{.Code}} {{.Method}} " +
        "{{.Path}} {{.Latency}}\n",
})
```

A custom formatter is any type implementing `accesslog.Formatter`
(`SetOutput(io.Writer)` plus `Format(*accesslog.Log) (bool, error)`).

### Custom Fields

Instance-level fields are computed for every request through `AddFields`,
as in the `request_id` example above. Handlers add per-request fields
through `accesslog.GetFields`:

```go
func chargeHandler(ctx iris.Context) {
    fields := accesslog.GetFields(ctx)
    fields.Set("tenant", ctx.Params().GetString("tenant"))
    fields.Set("items_charged", 3)

    ctx.JSON(iris.Map{"ok": true})
}
```

Fields land in the record's `fields` array as `{"key": ..., "value": ...}`
entries, so your log pipeline can index them like any other attribute.

### Skipping Noise and Logging Only Failures

Health checks and static assets can drown a log. Skip them per route
with `accesslog.SkipHandler` as a middleware, or conditionally with
`accesslog.Skip(ctx)`:

```go
app.Get("/ready", accesslog.SkipHandler, readyHandler)

app.Get("/assets/{file:path}", func(ctx iris.Context) {
    accesslog.Skip(ctx)
    // serve the asset...
})
```

Setting `ac.OnlyFailures = true` logs only requests that end with a
status code of 400 or above, a useful mode for a second, dedicated
error log alongside your metrics.

### Shutdown Semantics

`Close` flushes buffers, waits up to ten seconds for pending
asynchronous lines (`CloseContext` accepts a custom deadline) and closes
the underlying file. In manual apps, `defer ac.Close()`; the middleware
also registers its own `Close` on the interrupt signal. With the
builder, `LogRequestsWith` registers the closer for you:

```go
ac := newAccessLog()

app := iris.NewBuilder().
    Prefix("/api").
    AllowOrigin("none").
    Compression(true).
    LogRequestsWith(ac). // your tuned instance, closed on shutdown
    Health(true, "production", "ops@example.com").
    Errors(errors.NewOptions()).
    Deferrables().
    Services( /* your services */ ).
    RouterMiddlewares(requestid.New(requestid.ServerOnlyGenerator)).
    Middlewares().
    API("/orders", NewOrdersAPI).
    Build()
```

`LogRequests(true)` installs the framework's tuned default instead:
convenient in development, but it logs request and response bodies,
which is rarely what production wants. Prefer `LogRequestsWith` with a
configuration like the one above.

### Log Rotation

The access log writes to whatever `io.Writer` you give it, so rotation
plugs in at the writer level. With the third-party
`gopkg.in/natefinch/lumberjack.v2` package (clearly a third-party
choice, any rotating writer works):

```go
import "gopkg.in/natefinch/lumberjack.v2"

ac := accesslog.New(&lumberjack.Logger{
    Filename:   "/var/log/myapp/access_log.json",
    MaxSize:    100, // megabytes per file
    MaxBackups: 7,
    MaxAge:     28, // days
    Compress:   true,
})
```

If an external rotator (logrotate, a sidecar) truncates the file
instead, the middleware exposes `ac.TruncateFile(0)` to clear the
current file in place without reopening it.

### Live Tail with the Broker

The broker broadcasts every `accesslog.Log` to registered listeners,
the building block for a live log view over SSE or WebSocket, or for an
in-process alerting hook. Create it before the server starts:

```go
broker := ac.Broker()

go func() {
    logs := broker.NewListener() // read-only channel of accesslog.Log
    for log := range logs {
        if log.Code >= 500 {
            // push to an alert channel, a websocket, a counter...
        }
    }
}()
```

Call `broker.CloseListener(logs)` when a consumer goes away; `Close` on
the access log terminates all listeners.

The bare `go func()` above is the shortest way to show the channel. In a
builder application, wrap the loop in an `iris.Worker` instead (the
`CPUAlerter` further down this chapter is the shape) and pass it to
`Services`: the listener then starts after the initializers, stops on the
shutdown context, and `Shutdown` waits for it, none of which a loose
goroutine gives you.

## Request Correlation

One request produces many log lines: the access log record, application
log entries, database logs, entries on downstream services. A
correlation ID ties them together. The `requestid` middleware assigns
one per request, stores it on the context and reflects it in the
`X-Request-Id` response header:

```go
import "github.com/kataras/iris/v14/middleware/requestid"

app.Use(requestid.New())

app.Get("/", func(ctx iris.Context) {
    id := requestid.Get(ctx) // "" if the middleware did not run
    ctx.JSON(iris.Map{"request_id": id})
})
```

The default generator honors a client-supplied `X-Request-Id` header and
generates a UUIDv4 only when it is missing. Behind a trusted proxy that
assigns IDs, that is exactly right. Facing untrusted clients it is a log
injection vector: anyone can choose the value that lands in your logs.
When IDs must be unforgeable, generate them server-side only:

```go
app.Use(requestid.New(requestid.ServerOnlyGenerator))
```

Because the ID travels in the response header, a client (or your own
outgoing HTTP calls, by setting the same header on downstream requests)
can quote it in bug reports, and support can grep every system for it.
The access log picks it up through the `AddFields` setter shown earlier;
your application logger should include it too, which is where the next
section comes in.

That setter is worth a second look, because at first glance it should not
work. The access log is registered with `UseRouter`, *before*
`requestid.New`, so when its handler runs there is no ID yet. The reason it
sees one anyway is that field setters do not run on the way in. They run in
the access log's second pass, after the chain has returned:

\[Diagram: The access log starts its timer before the chain and writes its record after it, and because its field setters run in that second pass they can read a request ID that a later middleware generated.]

## Application Logging

The access log records what happened to requests; the application logger
records what your code decided along the way. Every Iris application
carries a [golog](https://github.com/kataras/golog) logger, reachable as
`app.Logger()` at startup and `ctx.Application().Logger()` inside
handlers:

```go
package main

import (
    "os"

    "github.com/kataras/golog"
    "github.com/kataras/iris/v14"
)

func main() {
    app := iris.New()

    logger := app.Logger()
    logger.SetLevel("info") // "fatal","error","warn","info","debug"
    logger.SetTimeFormat("2006-01-02 15:04:05")

    // Keep stdout and add a file output.
    f, err := os.OpenFile("app.log",
        os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
    if err != nil {
        logger.Fatalf("open log file: %v", err)
    }
    defer f.Close()
    logger.AddOutput(f)

    app.Get("/signup", func(ctx iris.Context) {
        ctx.Application().Logger().Infof("user signed up",
            golog.Fields{
                "plan":   "free",
                "source": "landing-page",
            })
        ctx.JSON(iris.Map{"ok": true})
    })

    app.Listen(":8080")
}
```

The leveled methods come in pairs (`Debug`/`Debugf`, `Info`/`Infof`,
`Warn`/`Warnf`, `Error`/`Errorf`, `Fatal`/`Fatalf`), and any
`golog.Fields` value passed among the arguments becomes structured
fields on the entry rather than format arguments. To emit the
application log itself as JSON (so it flows through the same pipeline as
the access log), set the formatter:

```go
app.Logger().SetFormat("json")
```

`SetOutput(w)` replaces all outputs with one writer, `AddOutput(w...)`
appends. At `SetLevel("debug")` Iris also prints route registrations and
other framework internals: useful in development, noisy in production;
`"info"` is the production default worth keeping.

## The Monitor Dashboard

The `middleware/monitor` package samples process and operating system
statistics (CPU, RAM, TCP connections, load average) on a fixed
interval and serves them two ways: as JSON and as a self-refreshing HTML
dashboard. The dashboard page fetches its numbers with a `POST` request
to its own URL, so the two handlers mount on the same path:

```go
package main

import (
    "time"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/basicauth"
    "github.com/kataras/iris/v14/middleware/monitor"
)

func main() {
    app := iris.New()

    m := monitor.New(monitor.Options{
        RefreshInterval:     2 * time.Second, // stats collection
        ViewRefreshInterval: 2 * time.Second, // dashboard refresh
        ViewTitle:           "My App",
    })
    iris.RegisterOnInterrupt(m.Stop)

    admin := app.Router("/admin", basicauth.Default(map[string]string{
        "admin": "change-me",
    }))
    admin.Get("/monitor", m.View)   // HTML dashboard
    admin.Post("/monitor", m.Stats) // JSON stats (used by the view)

    app.Listen(":8080")
}
```

Three practical notes. First, `RefreshInterval` left unset defaults
to `monitor.DefaultRefreshInterval` (2 seconds), so a zero-value
`monitor.Options{}` is safe; set it yourself when the collection
cadence matters. Second, the dashboard exposes live host
metrics:
mount it behind authentication, as above, never publicly. Third, stop
the collector on shutdown: `iris.RegisterOnInterrupt(m.Stop)` in manual
apps, `Deferrables(m.Stop)` in builder apps.

`Options` also accepts `PID` (defaults to the current process) and
`ViewAnimationInterval` for the chart animation.

### Reading the Stats Programmatically

The same numbers the dashboard renders are available in code, which
turns the monitor into a simple in-process alerting source. A sampling
loop is long-running work, so it belongs in an `iris.Worker` rather than
a goroutine started next to the server: the builder starts `Run` after
the initializers, cancels its context at shutdown, and waits for it to
return ([Chapter 3](https://iris-go.com/docs/iris-newbuilder/)). A bare `go func()` gets none
of that, and a `time.Tick` inside one leaks its ticker for the life of
the process.

```go
// CPUAlerter watches the numbers the dashboard renders and warns when
// the host is hot.
type CPUAlerter struct {
    Monitor  *monitor.Monitor
    Logger   *golog.Logger
    Interval time.Duration
}

func (a *CPUAlerter) Run(ctx context.Context) error {
    ticker := time.NewTicker(a.Interval)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return nil // shutdown: stop sampling.
        case <-ticker.C:
            if stats := a.Monitor.Holder.GetStats(); stats.OSCPU > 90 {
                a.Logger.Warnf("high CPU: %.1f%%", stats.OSCPU)
            }
        }
    }
}

// Stop releases what Run acquired. The ticker is Run's own, so there is
// nothing left to release here.
func (a *CPUAlerter) Stop(context.Context) error { return nil }
```

Register it with the monitor it reads:

```go
Deferrables(m.Stop).
Services(&CPUAlerter{
    Monitor:  m,
    Logger:   golog.Default,
    Interval: 30 * time.Second,
}).
```

`monitor.Stats` carries `PIDCPU`, `PIDRAM`, `PIDConns` for the process
and `OSCPU`, `OSRAM`, `OSTotalRAM`, `OSLoadAvg`, `OSConns` for the host.
Each value is also published through the standard `expvar` package under
`pid_cpu`, `pid_ram`, `pid_conns`, `os_cpu`, `os_ram`, `os_total_ram`,
`os_load_avg` and `os_conns`; see the
[third-party section](#third-party-metrics-and-tracing) for exposing
them over HTTP.

## Profiling with pprof

A profiler watches a running program and reports where its resources
actually go, attributed to functions: which code burns the CPU, which
allocates the memory, where goroutines sit blocked. Go's profiler, pprof,
is built into the runtime and works by sampling (briefly recording call
stacks many times per second), so it is safe to leave available in
production and enable on demand.

When the monitor tells you *that* CPU or memory is high, the Go
profiler tells you *why*. The `middleware/pprof` package mounts the
standard `net/http/pprof` endpoints as a routing controller. Profiles
reveal source file names and execution details, so guard them exactly
like the monitor:

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/basicauth"
    "github.com/kataras/iris/v14/middleware/pprof"
)

func main() {
    app := iris.New()

    auth := basicauth.Default(map[string]string{"ops": "change-me"})
    app.Router("/debug/pprof", auth, pprof.New())

    app.Listen(":8080")
}
```

`GET /debug/pprof` renders an index of the available profiles (heap,
goroutine, allocs, block, mutex, threadcreate, cmdline, profile, trace).
The interesting ones connect straight to the Go toolchain:

```sh
# 30-second CPU profile, interactive analysis
go tool pprof http://ops:change-me@localhost:8080/debug/pprof/profile

# heap snapshot
go tool pprof http://ops:change-me@localhost:8080/debug/pprof/heap

# 5-second execution trace
curl -u ops:change-me -o trace.out \
    "http://localhost:8080/debug/pprof/trace?seconds=5"
go tool trace trace.out
```

An alternative to permanent credentials is registering the routes only
when a debug flag or environment variable is set, so production builds
do not carry the endpoints at all. [Chapter 20](https://iris-go.com/docs/performance-optimization/)
walks through interpreting the profiles.

## Per-Request Cost Measurement

The `middleware/httpcost` package answers a question most stacks cannot:
what does one request cost? It measures elapsed time, CPU time, memory
allocation delta and bytes written, then folds them into a single number
using per-unit prices you define, the basis for usage-based billing,
tenant showback or simply spotting expensive endpoints:

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/httpcost"
)

func main() {
    app := iris.New()

    app.Use(httpcost.New(httpcost.Options{
        CostPerSecond: 0.01,  // per wall-clock second
        CostPerMB:     0.001, // per MB of RAM and network
        CostPerCPU:    0.02,  // per CPU-second
    }))

    app.Get("/reports", func(ctx iris.Context) {
        ctx.JSON(iris.Map{"report": "..."})
    })

    app.Listen(":8080")
}
```

`httpcost.DefaultOptions` carries those same values; setting a price to
zero disables that dimension, and at least one must remain enabled. The
middleware stores an `*httpcost.Metric` on the context *after* the
handler chain returns, so read it from something that wraps the chain;
the access log's field setters are the natural place:

```go
ac.AddFields(func(ctx iris.Context, f *accesslog.Fields) {
    if metric, ok := httpcost.Get(ctx); ok {
        f.Set("cost", metric.Cost)
        f.Set("cpu_time", metric.CpuTime)      // seconds
        f.Set("memory_used", metric.MemoryUsed) // bytes
    }
})
```

The metric also carries `StartTime`, `Duration` and `NetworkUsed` (the
bytes written to the response). Be honest about precision: CPU and
memory are sampled process- and system-wide, so under concurrency each
request is attributed a share of everything that ran at the same time.
Treat the numbers as consistent approximations, not an invoice-grade
meter. Outside HTTP, the same measurement runs over any function:

```go
cost := httpcost.Compute(func() {
    // an import job, a report build...
})
```

## Server-Timing

The `middleware/servertiming` package emits the standard `Server-Timing`
response header, which browser devtools render as a waterfall of your
server-side phases, free instrumentation for anyone with the network
tab open:

```go
import "github.com/kataras/iris/v14/middleware/servertiming"

app.Use(servertiming.Handler)

app.Get("/dashboard", func(ctx iris.Context) {
    servertiming.Start(ctx, "db", "Load records")
    // ... query the database ...
    servertiming.End(ctx, "db")

    servertiming.Start(ctx, "render", "Build payload")
    // ... assemble the response ...
    servertiming.End(ctx, "render")

    ctx.JSON(iris.Map{"data": "..."})
})
```

`servertiming.Handler` records the total as an `app` metric on its own.
Named metrics are optional refinement: every `Start` must be matched by
an `End` with the same name, and the header is written once all started
metrics have ended, before any body reaches the client. Timings are
visible to whoever receives the response, so keep metric names and
descriptions free of internal detail on public endpoints.

## Health Checks and Build Information

Load balancers, container orchestrators and uptime monitors all need a
cheap endpoint that answers "is this process alive, and which build is
it?". With the builder, one step provides it:

```go
Health(true, "production", "ops@example.com")
```

This registers `GET /health` at the root (deliberately outside the
`Prefix`), handled by the `middleware/modrevision` package. The response
is plain text: the server name and environment, the git revision and
build time when available, cloud container details when detected, and a
final `OK`. The revision and time come from `core/pkg/buildinfo`, which
reads the `vcs.revision` and `vcs.time` stamps Go embeds when you
`go build` inside a version-controlled checkout; `go run` or
`-buildvcs=false` builds simply omit them.

Outside the builder, mount the same handler yourself with full control
over its options:

```go
import "github.com/kataras/iris/v14/middleware/modrevision"

app.Get("/health", modrevision.New(modrevision.Options{
    ServerName:     "Orders API",
    Env:            "production",
    Developer:      "ops@example.com",
    UnixTime:       false,
    CloudDetection: true,
}))
```

The endpoint reveals environment and contact information by design. If
that is more exposure than your security policy allows, use `NoHealth()`
in the builder chain and register a terser endpoint of your own.

### Liveness Versus Readiness

`/health` is a liveness check: it answers as long as the process serves
HTTP, dependencies aside, exactly what a restart-deciding orchestrator
should watch. A readiness check asks a different question ("should this
instance receive traffic right now?") and must consult dependencies.
That is plain handler code:

```go
import (
    "database/sql"

    "github.com/kataras/iris/v14/rest/errors"
)

func readyHandler(db *sql.DB) iris.Handler {
    return func(ctx iris.Context) {
        if err := db.Ping(); err != nil {
            // 503 UNAVAILABLE, with the ping error logged and not sent.
            errors.Unavailable.LogErr(ctx, err)
            return
        }
        ctx.OK(iris.Map{"status": "ok"}, nil)
    }
}

app.Get("/ready", accesslog.SkipHandler, readyHandler(db))
```

Keep readiness checks fast and side-effect free (they run every few
seconds) and skip them in the access log as shown, or they become the
loudest client you have.

## Third-Party Metrics and Tracing

Everything above is in-tree. Two observability pillars deliberately are
not: time-series metrics (Prometheus and friends) and distributed
tracing (OpenTelemetry). Iris ships no exporter or integration layer for
them, and this section shows no invented one. What Iris does provide is
`iris.FromStd`, which mounts any standard `net/http` handler. That is
all the glue these ecosystems need.

Prometheus is the de facto standard metrics system: a separate server
that stores time series (numbers with timestamps) and evaluates alert
rules over them. It works by *pull*: on a fixed interval it fetches
("scrapes") a plain-text endpoint on your application, conventionally
`/metrics`, where your process publishes its current counters and
gauges. Your application never sends anything; it only answers when
scraped.

Exposing a Prometheus scrape endpoint with the official client library
(`github.com/prometheus/client_golang`):

```go
import (
    "github.com/prometheus/client_golang/prometheus/promhttp"

    "github.com/kataras/iris/v14"
)

app.Any("/metrics", iris.FromStd(promhttp.Handler()))
```

Instrumentation itself is ordinary middleware: register counters and
histograms with the Prometheus client and observe them in an `app.Use`
handler around `ctx.Next()`, using `ctx.GetStatusCode()`, `ctx.Path()`
and your own timer. No Iris-specific API is involved, so the client
library's documentation applies as-is.

Go's standard library has a minimal cousin of this idea: `expvar` lets
any package publish named variables, and its handler serves them all as
one JSON document over HTTP. The endpoint mounts the same way, and it is
worth mounting when the monitor middleware runs, because the monitor
publishes its gauges through `expvar`:

```go
import "expvar"

app.Any("/debug/vars", iris.FromStd(expvar.Handler()))
```

For distributed tracing, the OpenTelemetry Go SDK instruments at the
`net/http` layer; anything it produces as an `http.Handler` mounts
through `iris.FromStd` the same way, and its context propagation flows
through the standard request context that Iris hands to your code.
Protect `/metrics` and `/debug/vars` like the monitor and profiler:
behind authentication or reachable only from your internal network.

## Summary

- `accesslog` is the backbone: JSON lines to a file, `Async` on, bodies
  off, custom fields for the request ID and anything else your pipeline
  indexes. Register with `UseRouter`, close on shutdown, plug a rotating
  writer into `accesslog.New` for rotation. `LogRequestsWith(ac)` is the
  builder slot for a tuned instance.
- `requestid` correlates every log line of a request;
  `ServerOnlyGenerator` keeps the IDs unforgeable.
- `app.Logger()` is a leveled golog logger; `golog.Fields` adds
  structure and `SetFormat("json")` matches the access log's pipeline.
- `monitor` serves live process/OS stats as JSON and as a dashboard:
  `GET` and `POST` on the same path, behind admin authentication, with
  a positive `RefreshInterval` and `Stop` on shutdown.
- `pprof` mounts the Go profiler at `/debug/pprof`; `httpcost` prices
  each request; `servertiming` surfaces server phases in browser
  devtools.
- `Health(true, env, dev)` registers `GET /health` at the root via
  `modrevision`, reporting the git revision and build time embedded by
  `go build`; readiness checks that ping dependencies are plain
  handlers.
- Metrics and tracing belong to Prometheus and OpenTelemetry;
  `iris.FromStd(promhttp.Handler())` is the only glue required.

## Further Reading

- [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html):
  what to log, what never to log, and how to keep attacker-controlled
  input from corrupting your logs.
- [The Go net/http/pprof package](https://pkg.go.dev/net/http/pprof):
  the standard profiling endpoints that `middleware/pprof` mounts.
- [The Go expvar package](https://pkg.go.dev/expvar): the standard
  mechanism behind `/debug/vars` and the monitor's published gauges.
- [MDN: Server-Timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing):
  the header format and how browser devtools display it.
- [Prometheus Go client](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus):
  counters, gauges and histograms for the `/metrics` endpoint shown in
  this chapter.
- [OpenTelemetry Go SDK](https://pkg.go.dev/go.opentelemetry.io/otel):
  the tracing SDK whose `net/http` instrumentation mounts through
  `iris.FromStd`.
