# Go (net/http, chi, Gin)

> Monitor a Go service: a net/http middleware logging one JSON line per request with slog, security events through slog, and beam monitor with the Go rules.

Source: https://docs.agentbeam.com/cookbooks/frameworks/go

## How Beam reads a Go app

There's no in-process SDK for this stack, so the app writes what Beam needs to its logs and [`beam monitor`](https://docs.agentbeam.com/agent-cli/app-monitoring#install-and-start) reads them on the same server or container. Only redacted findings leave it. Two kinds of line:

- **One JSON line per request**, with `method`, `path` (with the query string), `status`, `ip`, `user_agent`, `host` and `duration_ms`. The status is what lets Beam tell an attack your app refused from one it answered.
- **Security events as plain lines**, worded like this (Beam matches the wording and reads `user=` and `ip=`):

```text
login failed user=42 ip=203.0.113.7
login succeeded user=42 ip=203.0.113.7
granted admin role to user=99 by user=12
API key created user=12
{"event":"llm_usage","user":"42","model":"gpt-5","total_tokens":1834}
```

Use an opaque user id, never an email. New outbound hosts are only reported by the Node.js SDK; for this stack, restrict outbound traffic at the network instead.

## Before you start

Do steps 1 and 2 of the [general cookbook](https://docs.agentbeam.com/cookbooks/app-monitoring#step-1-turn-it-on-and-create-the-apps-token) first: App monitoring is on, the app is added, and its `beam_app_` token is in your secret store as `BEAM_APP_TOKEN`. Everything below needs `@agent-beam/beam` 0.2.32 or newer.

## Log each request with its status

```go
// beam.go
package main

import (
	"log/slog"
	"net"
	"net/http"
	"os"
	"strings"
	"time"
)

var requestLog = slog.New(slog.NewJSONHandler(mustOpen("/var/log/app/requests.log"), nil))
var securityLog = slog.New(slog.NewJSONHandler(mustOpen("/var/log/app/security.log"), nil))

func mustOpen(path string) *os.File {
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o640)
	if err != nil {
		panic(err)
	}
	return f
}

type statusWriter struct {
	http.ResponseWriter
	status int
}

func (w *statusWriter) WriteHeader(code int) { w.status = code; w.ResponseWriter.WriteHeader(code) }

func clientIP(r *http.Request) string {
	if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { // only behind a proxy you control
		return strings.TrimSpace(strings.Split(fwd, ",")[0])
	}
	host, _, _ := net.SplitHostPort(r.RemoteAddr)
	return host
}

func BeamRequests(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		started := time.Now()
		sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(sw, r)
		requestLog.Info("request",
			"method", r.Method,
			"path", r.URL.RequestURI(),
			"status", sw.status,
			"ip", clientIP(r),
			"user_agent", r.UserAgent(),
			"host", r.Host,
			"duration_ms", time.Since(started).Milliseconds(),
		)
	})
}
```

Wrap your router: `http.ListenAndServe(":8080", BeamRequests(mux))`. With chi, `r.Use(BeamRequests)`; with Gin, write the same as a `gin.HandlerFunc` using `c.Writer.Status()` after `c.Next()`.

## Log sign-ins, admins and keys

```go
securityLog.Warn("login failed", "user", fmt.Sprint(userID), "ip", clientIP(r))
securityLog.Info("login succeeded", "user", fmt.Sprint(user.ID), "ip", clientIP(r))
securityLog.Info("granted admin role", "user", fmt.Sprint(target.ID), "by", fmt.Sprint(actor.ID))
securityLog.Info("API key created", "user", fmt.Sprint(user.ID))
securityLog.Info("llm usage", "user", fmt.Sprint(user.ID), "model", model, "total_tokens", usage.TotalTokens)
```

slog writes these as JSON; Beam reads the message and the `user`, `ip` and `total_tokens` fields. Pass the user id as a string (`fmt.Sprint`): a number isn't read as a user.

## Run `beam monitor` next to the app

```sh
npm install -g @agent-beam/beam
beam monitor --stack go --file /var/log/app/requests.log --file /var/log/app/security.log --app /srv/app
```

`--stack go` turns on this framework's rules and rolls up attacks on frameworks you don't run; `--app` also checks the app's lockfiles daily (and would detect the stack on its own). Keep it running with systemd:

```ini
# /etc/systemd/system/beam-monitor.service
[Unit]
Description=AgentBeam App monitoring
After=network-online.target

[Service]
EnvironmentFile=/etc/beam/app.env
ExecStart=/usr/bin/env beam monitor --stack go --file /var/log/app/requests.log --file /var/log/app/security.log --app /srv/app
Restart=always
User=beam

[Install]
WantedBy=multi-user.target
```

```sh
sudo install -m 600 /dev/null /etc/beam/app.env && echo "BEAM_APP_TOKEN=beam_app_..." | sudo tee /etc/beam/app.env >/dev/null
sudo systemctl daemon-reload && sudo systemctl enable --now beam-monitor
```

In Docker or Kubernetes, run the same command in a sidecar that mounts the log directory read-only, with `BEAM_APP_TOKEN` from a secret.
## Prove it works

```sh
beam monitor status   # app, level, stack, usage
beam monitor test     # a test finding through your alert workflows
curl -s "https://your-app.example.com/?id=1%20UNION%20SELECT%201" -o /dev/null
```

Within a minute, the SQL injection test shows on **App monitoring**: under **Needs action** if the app answered 2xx, or counted under "needed nothing from you" if it refused it. Then sign in to a test account with a wrong password ten times and the right one once: **Sign-in succeeded after 10 failed attempts** appears as critical.
## Check dependencies on every pull request

```yaml
# .github/workflows/beam-cve.yml
name: Dependency check
on: [pull_request]
jobs:
  cve:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx -y @agent-beam/beam cve check . --fail-on high
        env:
          BEAM_APP_TOKEN: ${{ secrets.BEAM_APP_TOKEN }}
```

`go.mod` is checked.
## What Beam watches for on this stack

Besides every [built-in rule](https://docs.agentbeam.com/agent-cli/app-monitoring#levels-and-detection) and the [attack-in-progress signals](https://docs.agentbeam.com/agent-cli/app-monitoring#signs-an-attack-is-underway):

| Rule | Means | Severity |
| --- | --- | --- |
| Go pprof or expvar served | `/debug/pprof` or `/debug/vars` answered 2xx: memory, goroutines and variables for anyone | High |

Attacks on other frameworks (WordPress probes, Laravel or Spring exploits, and so on) are rolled up as **Not your stack** when there's no status to say otherwise, and never alert.
## Close what Beam would catch

Beam raises these when they're found; better that there's nothing to find:

- Never import `net/http/pprof` into a binary that serves `http.DefaultServeMux` publicly; register it on a separate, internal listener.
- Set `ReadHeaderTimeout` and the other timeouts on your `http.Server`.
## Coverage checklist

| Feature | Done when |
| --- | --- |
| Request log with status | Findings show **Answered**, **Refused** or **Errored** |
| Stack declared | `beam monitor` prints the stack at start |
| Sign-in events | `login failed` and `login succeeded` lines are written |
| Admin and key events | Role grants and new keys are logged |
| LLM spend | An `llm_usage` line follows each model call |
| Dependency checks | The CI job runs on pull requests |
| Alerts | Critical pages someone, high reaches a channel ([step 8](https://docs.agentbeam.com/cookbooks/app-monitoring#step-8-send-alerts-to-the-right-place)) |
| Routine | Someone reads **Needs action** daily and **Attack analytics** weekly |
