|
| 1 | +# AGENTS.md — logging-go |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Shared logging library for Fishbrain's Go services. Single-package Go module (`package logging`) that wraps [logrus](https://github.com/sirupsen/logrus) with Bugsnag error reporting, Sentry error reporting, Datadog trace correlation, and NSQ log-level bridging. |
| 6 | + |
| 7 | +**Module path**: `github.com/fishbrain/logging-go` |
| 8 | + |
| 9 | +## Commands |
| 10 | + |
| 11 | +| Task | Command | |
| 12 | +|-------|------------------| |
| 13 | +| Build | `go build ./...` | |
| 14 | +| Test | `go test ./...` | |
| 15 | + |
| 16 | +There is no linter, formatter, or Makefile configured. CI (`go.yml`) runs `go build -v .` only — no test step in CI. |
| 17 | + |
| 18 | +## Project Structure |
| 19 | + |
| 20 | +``` |
| 21 | +logging.go # All library code — types, logger init, entry helpers, Bugsnag/Sentry hooks |
| 22 | +logging_test.go # All tests |
| 23 | +go.mod / go.sum # Module definition (Go 1.24+, toolchain 1.26) |
| 24 | +.tool-versions # asdf version pinning (go 1.26.0) |
| 25 | +``` |
| 26 | + |
| 27 | +This is a **single-file library** — everything lives in `logging.go` and `logging_test.go`. No subdirectories, no `cmd/`, no `internal/`. |
| 28 | + |
| 29 | +## Architecture & Key Types |
| 30 | + |
| 31 | +### Global singleton |
| 32 | + |
| 33 | +`Init(LoggingConfig)` initializes the package-level `Log *Logger` variable. It is guarded by a nil check (not a `sync.Once`), so it only runs once. `TestMain` calls `Init(LoggingConfig{})` to set up the singleton before tests run. |
| 34 | + |
| 35 | +### Type hierarchy |
| 36 | + |
| 37 | +- **`Logger`** — wraps `*logrus.Logger`. Provides `WithField`, `WithError`, `WithDDTrace`, `NewEntry`, and `NSQLogger`. |
| 38 | +- **`Entry`** — wraps `*logrus.Entry`. Provides domain-specific field helpers (`WithUser`, `WithEvent`, `WithChannel`, `WithDuration`, etc.) that return `*Entry` for chaining. |
| 39 | +- **`NSQLogger`** — adaptor that implements `Output(int, string) error` so it can be passed to `nsq.SetLogger`. |
| 40 | +- **`bugsnagHook`** — logrus hook that fires on Error/Fatal/Panic levels, forwarding to Bugsnag with metadata. |
| 41 | +- **`sentryHook`** — logrus hook that fires on Error/Fatal/Panic levels, forwarding to Sentry with metadata and extra fields. |
| 42 | + |
| 43 | +### Initialization flow |
| 44 | + |
| 45 | +``` |
| 46 | +Init(config) → |
| 47 | + 1. bugsnag.Configure(...) — sets up Bugsnag client |
| 48 | + 2. bugsnag.OnBeforeNotify(...) — unwraps *fmt.wrapError to get real error class |
| 49 | + 3. sentry.Init(...) — sets up Sentry client (if SentryDSN is set and environment matches ErrorNotifyReleaseStages) |
| 50 | + 4. Log = new(true, withSentry, config) — creates Logger with Bugsnag and optionally Sentry hooks attached |
| 51 | +``` |
| 52 | + |
| 53 | +## Key Dependencies |
| 54 | + |
| 55 | +| Dependency | Purpose | |
| 56 | +|---|---| |
| 57 | +| `github.com/sirupsen/logrus` | Structured logging (JSON formatter) | |
| 58 | +| `github.com/bugsnag/bugsnag-go/v2` | Error reporting to Bugsnag | |
| 59 | +| `github.com/DataDog/dd-trace-go/v2` | Datadog APM trace/span ID injection | |
| 60 | +| `github.com/getsentry/sentry-go` | Error reporting to Sentry | |
| 61 | +| `github.com/nsqio/go-nsq` | NSQ message queue log-level bridging | |
| 62 | +| `github.com/stretchr/testify` | Test assertions | |
| 63 | + |
| 64 | +## Code Patterns & Conventions |
| 65 | + |
| 66 | +### Fluent entry builder |
| 67 | + |
| 68 | +All `With*` methods return `*Entry` to support chaining: |
| 69 | + |
| 70 | +```go |
| 71 | +Log.WithDDTrace(ctx).WithUser(userID).WithDuration(d).Info("processed request") |
| 72 | +``` |
| 73 | + |
| 74 | +When adding new field helpers, follow this pattern: method on `*Entry`, return `*Entry`, delegate to `e.WithField(...)`. |
| 75 | + |
| 76 | +### Error wrapping |
| 77 | + |
| 78 | +Errors passed to `WithError` are wrapped with `bugsnag_errors.New(err, 1)` to capture stack traces. The `1` parameter controls stack frame skipping. The standalone `Errorf` and `ErrorWithStacktrace` functions also use this pattern. |
| 79 | + |
| 80 | +### JSON log output |
| 81 | + |
| 82 | +Logrus is configured with `JSONFormatter` and custom field mapping: |
| 83 | +- `msg` → `message` |
| 84 | +- `func` → `logger.method_name` |
| 85 | +- `file` → `logger.name` |
| 86 | +- `error` key → `error.message` |
| 87 | +- Timestamp format: `RFC3339Nano` |
| 88 | + |
| 89 | +### Log levels |
| 90 | + |
| 91 | +The `LogLevel` config string must be uppercase: `"ERROR"`, `"WARNING"`, `"INFO"`, `"DEBUG"`. Unknown values default to `InfoLevel`. |
| 92 | + |
| 93 | +### NSQ log bridging |
| 94 | + |
| 95 | +`Logger.NSQLogger()` returns an `(NSQLogger, nsq.LogLevel)` tuple for plugging into `nsq.SetLogger`. The `NSQLogger.Output` method parses the 3-character prefix from NSQ log messages to route them to the correct logrus level. |
| 96 | + |
| 97 | +## Testing |
| 98 | + |
| 99 | +- **Framework**: stdlib `testing` + `testify/assert` |
| 100 | +- **Setup**: `TestMain` initializes the global `Log` singleton via `Init(LoggingConfig{})` |
| 101 | +- **Log capture**: Tests use `os.Pipe()` to capture log output by swapping `Log.Out`, then assert on the captured string content |
| 102 | +- **Concurrency test**: `TestConcurrentUseOfEntry` verifies entries are safe for concurrent use across goroutines |
| 103 | +- **Table-driven tests**: `TestGetLogrusLogLevel` uses a table-driven approach with a package-level test data slice |
| 104 | +- **Sentry hook tests**: `TestSentryHookFire`, `TestSentryHookLevels`, `TestNewWithSentry`, and `TestNewWithoutSentry` cover the Sentry hook and its integration into the logger |
| 105 | +- **Release-stage gating tests**: `TestShouldNotify` verifies the `shouldNotify` helper used for conditional Sentry/Bugsnag activation |
| 106 | + |
| 107 | +## Gotchas |
| 108 | + |
| 109 | +1. **No CI test step**: The GitHub Actions workflow builds but does not run tests. Running `go test ./...` locally is essential before pushing. |
| 110 | +2. **Singleton guard is not sync.Once**: `Init` uses `if nil == Log` — safe for single-goroutine init, but not for concurrent callers. In practice this is fine since `Init` is called once at service startup. |
| 111 | +3. **`ioutil.ReadAll` in tests**: Tests use the deprecated `io/ioutil` package. New code should use `io.ReadAll` instead. |
| 112 | +4. **Bugsnag error unwrapping limit**: The `OnBeforeNotify` handler unwraps `*fmt.wrapError` chains up to 11 levels deep, then logs and stops. |
| 113 | +5. **`logrus.ErrorKey` is mutated globally**: `new()` sets `logrus.ErrorKey = "error.message"` as a side effect — this affects all logrus loggers in the process, not just this one. |
| 114 | +6. **Reversed nil check style**: The codebase uses Yoda conditions (`nil == Log`) in the `Init` function. |
| 115 | +7. **`BugsnagNotifyReleaseStages` renamed**: The config field was renamed to `ErrorNotifyReleaseStages` and is now shared between Bugsnag and Sentry for release-stage gating. |
| 116 | +8. **Sentry is conditional**: Sentry is only initialized when `SentryDSN` is non-empty and the current `Environment` is in `ErrorNotifyReleaseStages`. If `sentry.Init` fails, it logs to stderr and proceeds without the Sentry hook. |
| 117 | + |
| 118 | +## Releasing |
| 119 | + |
| 120 | +Create a GitHub Release. The module is imported by other Fishbrain Go services via its module path. Versioning follows Go module semantics (semver tags). |
| 121 | + |
| 122 | +## Ownership |
| 123 | + |
| 124 | +Owned by `@fishbrain/platform-team` (see `CODEOWNERS`). Dependency updates managed by Renovate (see `renovate.json`). |
0 commit comments