Skip to content

Logging

SPYDER uses Uber Zap for structured logging. All log output is JSON-formatted and written to stderr, making it straightforward to parse with jq, ship to log aggregation systems, or integrate with systemd journal.

Logger Initialization

SPYDER initializes a production Zap logger at startup:

go
// internal/logging/logging.go
package logging

import "go.uber.org/zap"

type Logger = zap.SugaredLogger

func New() *Logger {
    l, _ := zap.NewProduction()
    return l.Sugar()
}

The SugaredLogger provides a key-value style API used throughout the codebase:

go
log.Info("starting spyder", "probe", cfg.Probe, "run", cfg.Run, "concurrency", cfg.Concurrency)
log.Warnw("ingest failed, spooling", "err", err)
log.Debugw("robots.txt fetch", "host", host, "err", err)

Log Levels

SPYDER uses four log levels:

LevelUsageExamples
debugDetailed operational information for troubleshootingrobots.txt fetch results, link parsing errors, per-host details
infoNormal operational eventsstartup configuration, mode selection, shutdown
warnRecoverable problems that do not stop processingfailed ingest calls (data spooled), OTEL init failures, Redis errors
errorSerious failures requiring attentionspool file creation failures, unrecoverable errors

Setting the Log Level

Use the LOG_LEVEL environment variable to control log verbosity:

bash
# Show all logs including debug
LOG_LEVEL=debug ./bin/spyder -domains=domains.txt

# Default production level (info and above)
LOG_LEVEL=info ./bin/spyder -domains=domains.txt

# Warnings and errors only
LOG_LEVEL=warn ./bin/spyder -domains=domains.txt

# Errors only
LOG_LEVEL=error ./bin/spyder -domains=domains.txt

Verbose Mode

The -verbose flag provides an alternative way to enable debug-level logging:

bash
./bin/spyder -domains=domains.txt -verbose

This is equivalent to LOG_LEVEL=debug and is useful for quick debugging sessions without modifying environment variables.

JSON Log Format

All log lines are JSON objects written to stderr. The Zap production encoder produces output in this format:

json
{"level":"info","ts":1704067200.123,"caller":"spyder/main.go:362","msg":"starting spyder","probe":"local-1","run":"run-1704067200","concurrency":256,"continuous":false,"exclude_tlds":["gov","mil","int"],"config_file":""}

Standard Fields

Every log line contains these fields:

FieldTypeDescription
levelstringLog level (debug, info, warn, error)
tsfloatUnix timestamp with fractional seconds
callerstringSource file and line number
msgstringHuman-readable log message

Context Fields

Additional fields depend on the log message:

Startup logs:

json
{"level":"info","ts":1704067200.1,"msg":"starting spyder","probe":"prod-us-west","run":"scan-20240101","concurrency":512,"continuous":true,"exclude_tlds":["gov","mil","int"]}
{"level":"info","ts":1704067200.2,"msg":"redis dedupe enabled","addr":"redis.internal:6379"}
{"level":"info","ts":1704067200.3,"msg":"continuous mode enabled (in-memory)","max_domains":5000}
{"level":"info","ts":1704067200.4,"msg":"metrics and health server started","addr":":9090"}

Operational logs:

json
{"level":"debug","ts":1704067205.5,"msg":"robots.txt fetch","host":"example.com","err":"context deadline exceeded"}
{"level":"warn","ts":1704067210.8,"msg":"ingest failed, spooling","err":"Post \"https://ingest.internal/v1/batch\": dial tcp: connection refused"}
{"level":"warn","ts":1704067215.2,"msg":"redis dedup error","count":3,"err":"read tcp: i/o timeout"}

Shutdown logs:

json
{"level":"info","ts":1704067300.0,"msg":"service marked as ready"}
{"level":"info","ts":1704068400.0,"msg":"shutdown complete"}

Log Analysis with jq

Since all logs are JSON, jq is a natural tool for analysis.

Filter by Level

bash
# Show only warnings and errors
./bin/spyder -domains=domains.txt 2>&1 >/dev/null | \
  jq -r 'select(.level == "warn" or .level == "error") | "\(.ts | todate) [\(.level)] \(.msg)"'

Extract Error Summaries

bash
# Count errors by message
./bin/spyder -domains=domains.txt 2>&1 >/dev/null | \
  jq -r 'select(.level == "error" or .level == "warn") | .msg' | \
  sort | uniq -c | sort -rn

Filter by Component

bash
# Show only probe-related logs (by caller path)
./bin/spyder -domains=domains.txt 2>&1 >/dev/null | \
  jq -r 'select(.caller | contains("probe/")) | "\(.ts | todate) \(.msg) \(.host // "")"'

Monitor Ingest Failures

bash
# Watch for ingest failures in real time
./bin/spyder -domains=domains.txt 2>&1 >/dev/null | \
  jq -r 'select(.msg | contains("ingest failed")) | "\(.ts | todate) \(.err)"'

Track Redis Errors

bash
# Watch Redis dedup errors and their frequency
./bin/spyder -domains=domains.txt 2>&1 >/dev/null | \
  jq -r 'select(.msg | contains("redis")) | "\(.ts | todate) \(.msg) count=\(.count // "n/a") err=\(.err // "n/a")"'

Convert Timestamps

Zap uses Unix epoch timestamps. Convert them to human-readable format:

bash
./bin/spyder -domains=domains.txt 2>&1 >/dev/null | \
  jq -r '"\(.ts | todate) [\(.level | ascii_upcase)] \(.msg)"'

Separating Logs from Output

SPYDER writes JSON data output to stdout and logs to stderr. This separation is important for pipeline usage:

bash
# Capture data output to file, view logs on terminal
./bin/spyder -domains=domains.txt > output.json

# Capture logs to file, view data on terminal
./bin/spyder -domains=domains.txt 2> spyder.log

# Capture both separately
./bin/spyder -domains=domains.txt > output.json 2> spyder.log

# Pipe data output while monitoring logs
./bin/spyder -domains=domains.txt 2>/dev/null | jq '.edges | length'

When using the -ingest flag, stdout is not used for data output (data goes to the ingest endpoint), so logs on stderr are the primary operational output.

Journal Integration (systemd)

When running SPYDER as a systemd service, logs go directly to the journal:

systemd Service Configuration

ini
# /etc/systemd/system/spyder.service
[Service]
ExecStart=/opt/spyder/bin/spyder -domains=/etc/spyder/domains.txt -concurrency=256
StandardOutput=journal
StandardError=journal
SyslogIdentifier=spyder

Querying Journal Logs

bash
# View all SPYDER logs
journalctl -u spyder.service

# Follow logs in real time
journalctl -u spyder.service -f

# Show logs since last boot
journalctl -u spyder.service -b

# Show logs from the last hour
journalctl -u spyder.service --since "1 hour ago"

# Show only warnings and errors
journalctl -u spyder.service -p warning

# Export logs as JSON for jq processing
journalctl -u spyder.service -o json | \
  jq -r '.MESSAGE' | \
  jq -r 'select(.level == "warn") | "\(.ts | todate) \(.msg)"'

Journal Storage Configuration

For long-term log retention, configure journald:

ini
# /etc/systemd/journald.conf
[Journal]
Storage=persistent
SystemMaxUse=2G
MaxRetentionSec=90day
Compress=yes

Log Shipping

Forward to Elasticsearch/OpenSearch

Use journalbeat or filebeat to ship SPYDER logs to a search backend:

yaml
# filebeat.yml
filebeat.inputs:
- type: journald
  id: spyder-logs
  include_matches:
    - _SYSTEMD_UNIT=spyder.service

output.elasticsearch:
  hosts: ["https://elasticsearch.internal:9200"]
  index: "spyder-logs-%{+yyyy.MM.dd}"

processors:
- decode_json_fields:
    fields: ["message"]
    target: "spyder"
    overwrite_keys: true

Forward to Loki

For Grafana Loki integration, use promtail:

yaml
# promtail.yml
scrape_configs:
- job_name: spyder
  journal:
    labels:
      job: spyder
    matches: _SYSTEMD_UNIT=spyder.service
  relabel_configs:
  - source_labels: ['__journal__systemd_unit']
    target_label: unit
  pipeline_stages:
  - json:
      expressions:
        level: level
        msg: msg
  - labels:
      level:

Troubleshooting with Logs

Debug a Specific Domain

Run SPYDER in verbose mode with a single domain to see every step:

bash
echo "problem-domain.com" > debug.txt
./bin/spyder -domains=debug.txt -concurrency=1 -verbose 2>&1 >/dev/null | jq .

This shows debug-level logs for DNS resolution, robots.txt checking, HTTP fetching, TLS analysis, and link extraction for that single domain.

Identify Common Failure Patterns

bash
# Top error messages from a scan
./bin/spyder -domains=domains.txt 2>scan.log >/dev/null
jq -r 'select(.level == "warn" or .level == "error") | .msg' scan.log | \
  sort | uniq -c | sort -rn | head -10

Common patterns:

MessageMeaningAction
robots.txt fetch (debug)Could not retrieve robots.txtUsually benign; site may not have one
create request (warn)Invalid URL constructionCheck domain format in input file
ingest failed, spooling (warn)Ingest endpoint unreachableCheck network connectivity to ingest API
redis dedup error (warn)Redis connection issueCheck Redis availability and network
parse links (debug)HTML parsing failureUsually benign; non-standard HTML
otel init failed (warn)OTEL collector unreachableCheck -otel_endpoint configuration

Monitor Log Volume

High log volume can indicate problems (e.g., a Redis outage generating repeated warnings):

bash
# Count log lines per level over a time window
journalctl -u spyder.service --since "1 hour ago" -o json | \
  jq -r '.MESSAGE' | jq -r '.level' 2>/dev/null | \
  sort | uniq -c | sort -rn

Expected distribution for a healthy scan: mostly info at startup/shutdown, very few warn, and zero error. A flood of warn lines typically indicates an infrastructure issue (Redis down, ingest endpoint unreachable, or DNS resolver problems).