Search, ripgrep & jq for Debugging
Use ripgrep to hunt symbols across Go modules and jq to slice JSON logs into answers during incidents.
Summary
Debugging Go services means searching two haystacks: source in git and JSON lines on disk or in journal exports.
rg replaces slow recursive grep for code.
jq turns structured logs into filters, counts, and field extractions without a full log platform.
Recipe
Quick-reference recipe card - copy-paste ready.
# Code: find handler registration
rg -n "HandleFunc|Handle\(" --glob '*.go' internal/ cmd/
# Code: tests referencing a flaky function
rg -n "TestProcessOrder" -g '*_test.go'
# Logs: errors in last export
jq -c 'select(.level=="error")' app.json.log | head
# Logs: count by trace_id prefix
jq -r 'select(.level=="error") | .trace_id' app.json.log | cut -c1-8 | sort | uniq -c | sort -nrWhen to reach for this:
- You need every callsite of a deprecated function before refactoring.
- Production logs are JSON and you must isolate one
request_idquickly. - CI saved a log artifact and you are grep-ing failures offline.
Working Example
#!/usr/bin/env bash
set -euo pipefail
# --- Repository search ---
# Find where context cancellation is ignored in handlers
rg -n "context\.Background\(\)" --glob '*.go' -g '!*_test.go' internal/
# Find build tags or platform files
rg -n "//go:build" --glob '*.go'
# --- Log analysis (sample NDJSON file) ---
cat > /tmp/sample.json.log <<'EOF'
{"time":"2026-07-15T10:00:01Z","level":"info","msg":"start","service":"api","trace_id":"abc123"}
{"time":"2026-07-15T10:00:02Z","level":"error","msg":"db timeout","service":"api","trace_id":"abc123","err":"context deadline"}
{"time":"2026-07-15T10:00:03Z","level":"error","msg":"db timeout","service":"api","trace_id":"def456","err":"context deadline"}
EOF
# All errors for one trace
jq -c 'select(.trace_id=="abc123")' /tmp/sample.json.log
# Top error messages
jq -r 'select(.level=="error") | .msg' /tmp/sample.json.log | sort | uniq -c | sort -nr
# Combine rg on a journal export
journalctl -u api.service --since today --no-pager -o cat > /tmp/api.today.log
rg '"level":"error"' /tmp/api.today.log | jq -c . | headWhat this demonstrates:
- Scoped ripgrep with globs and negated test files.
- jq filters on NDJSON (one JSON object per line).
- Aggregations with Unix tools after
jq -rextracts a field. - Hybrid workflow when journal output is JSON-per-line.
Deep Dive
How It Works
- ripgrep walks the filesystem with regex engines in parallel, skips binary files, and honors
.gitignoreunless you pass--no-ignore. - jq parses JSON values; filters (
select), projections (.field), and slurp (-s) reshape streams for shell pipes. - Go structured logging (
slog, zap, zerolog) typically emits NDJSON or logfmt; NDJSON is jq-friendly when each line is valid JSON. - Pipelines stay in the shell: export logs, search, count, then jump to code with
rgon the handler named in the logmsg.
ripgrep Flags Worth Knowing
| Flag | Effect |
|---|---|
-n | Line numbers |
-l | Files with matches only |
-g '*.go' | Glob filter (repeatable) |
-g '!vendor/**' | Exclude paths |
-F | Fixed string (log needles) |
-C 3 | Context lines |
--type go | Built-in type filter |
jq Patterns for Logs
| Task | jq |
|---|---|
| Filter level | select(.level=="error") |
| Field exists | select(has("trace_id")) |
| Extract field | .trace_id with -r |
| Time window | select(.time >= "2026-07-15T10:00:00Z") |
| Array stats | `jq -s 'map(select(.level=="error")) |
Go Notes
// Prefer stable field names for jq filters
slog.Error("db timeout",
"level", "error", // redundant if handler sets level - be consistent
"trace_id", traceID,
"err", err.Error(),
)Align key names with dashboards (trace_id vs traceId) to avoid duplicate jq recipes.
Gotchas
- jq on non-JSON lines - Stack traces and printf logs break
jqparsers. Fix:rgfilter first, or usejq -R 'fromjson? | select(.)'to skip bad lines. - ripgrep searching vendor -
--no-ignorescans vendored deps. Fix: default ignore, or-g '!vendor/**'. - Case-sensitive level strings -
ERRORvserrormisses rows. Fix: normalize in Go logging config; usejqascii_downcaseif legacy logs vary. - Huge log slurp with
jq -s- Loads entire file into memory. Fix: stream line-by-line filters for GB-scale files. - Regex metacharacters in
rg- Dots inerr.context deadlineneed-For escaping. Fix:rg -F 'context deadline'. - Logging PII into jq pipelines - Extracting user emails into shell history is risky. Fix: redact at ingest; work on sanitized exports in incidents.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| IDE global search | Single-module navigation with click-to-open | Remote SSH session without IDE |
git grep | History bounded to tracked files | Untracked generated logs or large ignored dirs |
| Loki / Elasticsearch | Team-wide historical queries | You only have a 50MB file and five minutes |
ack / ag | Legacy laptops without rg | Greenfield - install ripgrep instead |
mlog / custom Go tool | Proprietary log format | Standard JSON already works with jq |
FAQs
Why ripgrep over git grep?
ripgrep respects gitignore by default, searches untracked trees when needed, and is faster on large monorepos.
git grep is fine for quick history-only searches.
Does jq require one JSON object per line?
That NDJSON shape is the common case for log files.
Multi-line pretty JSON needs different parsing or jq -s for small files.
How do I search zap or zerolog output?
If JSON keys match your filters, the same jq recipes apply.
For console-encoded logs, switch to JSON encoding in staging before incidents.
Can ripgrep replace find for Go files?
rg --files -g '*.go' lists paths; combine with fzf for interactive pickers.
Use find when metadata like mtime matters.
How do I debug multiline Go panics in logs?
Panics are often multi-line plain text.
Use rg -U multiline mode or read journal entries as units via journalctl -o json.
What if logs use logfmt instead of JSON?
rg still finds keys; jq does not apply.
Consider mlog, lnav, or converting a slice to JSON in a small Go script.
Is counting with jq slow?
Streaming counts with jq -c 'select(...)' | wc -l stays O(n) memory.
Avoid -s on gigabyte files.
How do I share a search recipe with my team?
Check in a scripts/debug/ shell snippet or document one-liners in the runbook next to dashboard links.
Can I use ripgrep inside docker?
Yes if the image includes rg, or docker exec into a debug sidecar.
Distroless runtime images often lack shell tools - copy logs out instead.
How do I find all TODOs in Go code?
rg -n "TODO\(|FIXME" --glob '*.go' with optional -g '!vendor/**'.
Does jq support Go duration strings?
jq does not parse Go's duration format natively.
Extract the string field and interpret in Go or convert at log emission time to numeric milliseconds.
What about sensitive data in shell history?
Use space-prefixed commands where HISTCONTROL ignores them, or run recipes from scripts that read files instead of inline customer IDs.
Related
- Linux CLI Basics - tail and jq intro
- Build, Test & Profile from the Shell - searching test output
- Linux CLI Skills for Go Engineers - log triage flow
- Shell Productivity: tmux, fzf & direnv - fzf + rg integration
- Observability - structured logging in Go
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).