Observability
Skill MARUCIE/openclaw-foundry/web/public/packs/spellbook-security-auditor/skills/observability
The curated AI Agent skill marketplace — 37K+ vetted skills, S/A/B/C ratings, deploy anywhere
npx -y skills add MARUCIE/openclaw-foundry --skill observabilityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when adding structured logging, instrumenting Prometheus metrics, wiring up distributed tracing with OpenTelemetry, writing alerting rules, defining SLOs and error budgets, or building a Grafana golden-signals dashboard.
SKILL.md
25.9 KB, as published. Nobody here has run it
是什么
这是一份从安全审计角度切入的可观测性规范,覆盖审计日志结构化、安全事件指标、可追溯链路、异常行为告警,让团队具备发生事故时可追责、可回溯、可取证的能力,而不是事后翻不到证据。
怎么用
- 审计敏感操作(登录、授权变更、数据导出)必须按文档模板打结构化日志,含 user_id、ip、operation 字段。
- 给鉴权失败、限流命中、异常访问三类事件配 Prometheus(监控指标系统)计数指标,超阈值立刻告警。
- 接入 OpenTelemetry(开放遥测标准)分布式追踪时,关键调用链必须打上 trace_id 并保留 30 天以上以备审计。
- 告警规则按合规要求(数据脱敏、最小披露)配置内容模板,避免告警本身泄漏敏感信息。
- 定期用安全大盘审视失败率与异常访问趋势,发现可疑模式立刻按事故响应流程介入。
架构图
flowchart LR
A[业务操作] --> B[审计日志]
A --> C[安全指标]
A --> D[追踪链路]
B & C & D --> E[安全大盘]
E --> F[异常告警]
Observability
Observability is the practice of instrumenting systems so you can understand their internal state from external outputs — logs, metrics, and traces.
When to Activate
- Adding logging to a new service or endpoint
- Setting up Prometheus metrics for a service
- Implementing distributed tracing across services
- Writing alerting rules or on-call runbooks
- Defining SLOs and error budgets for a service
- Building a Grafana dashboard for a service
- Debugging a production issue using logs, metrics, or traces
The Three Pillars
| Pillar | Question it answers | Best tool | When to reach for it |
|---|---|---|---|
| Logs | "What happened?" | structlog, pino, slog | Debugging specific errors, audit trails |
| Metrics | "How is the system performing?" | Prometheus | Trending, alerting, capacity |
| Traces | "Why is this slow / where did it fail?" | OpenTelemetry | Latency debugging, distributed request flow |
OpenTelemetry is the unifying standard across all three pillars: one SDK for logs, metrics, and traces, vendor-agnostic, with exporters to any backend (Grafana, Datadog, Honeycomb, Jaeger, etc.).
Structured Logging
Always emit logs as JSON. Human-readable plaintext is fine in development, but production logs must be machine-parseable. Never log sensitive data (PII, card numbers, passwords, tokens).
Mandatory Fields
Every log line must include:
| Field | Type | Example |
|---|---|---|
timestamp | ISO 8601 | 2024-01-15T10:30:00.000Z |
level | string | INFO |
service | string | payment-service |
trace_id | string | abc123... |
span_id | string | def456... |
request_id | string | UUID per HTTP request |
message | string | Human-readable description |
Log Levels
| Level | When to use | Example |
|---|---|---|
| DEBUG | Verbose dev-only detail | "Entering validatePayment()" |
| INFO | Normal operational events | "Payment processed for order 123" |
| WARN | Unexpected but recoverable | "Retry attempt 2/3 for order 123" |
| ERROR | Failure that needs attention | "Payment declined: card expired" |
| FATAL/CRITICAL | Service cannot continue | "DB connection pool exhausted" |
Rule: use INFO in production, DEBUG only in development. WARN does not page on-call. ERROR does.
Code Examples
Python with structlog:
import structlog
logger = structlog.get_logger()
# Configure once at startup
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
]
)
# Usage — bind context, then log
log = logger.bind(service="payment-service", request_id=request_id)
log.info("payment.processing", order_id=order_id, amount=amount)
log.error("payment.failed", order_id=order_id, error=str(e))
TypeScript with pino:
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
base: { service: 'payment-service' },
});
const requestLogger = logger.child({ requestId, traceId });
requestLogger.info({ orderId, amount }, 'payment.processing');
requestLogger.error({ orderId, err }, 'payment.failed');
Go with slog (stdlib, Go 1.21+):
import "log/slog"
logger := slog.With(
slog.String("service", "payment-service"),
slog.String("request_id", requestID),
)
logger.Info("payment.processing",
slog.String("order_id", orderID),
slog.Float64("amount", amount),
)
logger.Error("payment.failed",
slog.String("order_id", orderID),
slog.String("error", err.Error()),
)
Correlation ID Propagation
Correlation IDs let you follow a single request across multiple services and log entries.
- Generate
request_idat the edge (API gateway or the first handler to receive the request) - Pass it downstream via HTTP headers:
X-Request-IDorX-Correlation-ID - Include the ID in all log lines for the duration of that request
- Include the ID in error responses so users can report it to support
- In async flows (message queues, event streams): embed
correlation_idin the message payload itself, not just headers
Middleware pattern (Express/Node):
import { v4 as uuidv4 } from 'uuid';
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] as string ?? uuidv4();
req.requestId = requestId;
res.setHeader('X-Request-ID', requestId);
req.log = logger.child({ requestId });
next();
});
Middleware pattern (Python/FastAPI):
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
class CorrelationIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
with structlog.contextvars.bound_contextvars(request_id=request_id):
response = await call_next(request)
response.headers["x-request-id"] = request_id
return response
Metrics with Prometheus
Prometheus is a pull-based metrics system: your service exposes a /metrics endpoint and Prometheus scrapes it on a configured interval (typically 15–30 seconds).
Metric Types
| Type | Use for | Example |
|---|---|---|
| Counter | Things that only go up (requests, errors) | http_requests_total |
| Gauge | Values that go up and down | active_connections, memory_bytes |
| Histogram | Distribution of values (latency, size) | request_duration_seconds |
| Summary | Like histogram, but calculates quantiles client-side | Avoid — prefer Histogram |
Prefer Histogram over Summary. Histograms can be aggregated across instances in PromQL; Summaries cannot.
Naming Conventions
Pattern: <namespace>_<subsystem>_<name>_<unit>
payment_service_http_requests_total— counter;_totalsuffix implies countpayment_service_db_query_duration_seconds— histogram; always include unit in the namepayment_service_cache_hit_ratio— gauge; ratio 0–1, no unit suffix neededpayment_service_queue_depth— gauge; current number of items in queue
Never include label values in metric names. Use labels instead:
# WRONG
http_500_errors_total
# RIGHT
http_errors_total{status="500"}
Label Cardinality Trap
Labels multiply time series. A metric with two labels each having 10 values creates 100 series. High-cardinality labels (user IDs, order IDs, full URL paths with path parameters) can create millions of series, crashing Prometheus.
# BAD — creates a new time series for every unique user ID
http_requests_total.labels(user_id=user.id).inc()
# GOOD — low cardinality labels only
http_requests_total.labels(method="POST", endpoint="/payments", status="200").inc()
Safe label values: HTTP method, endpoint (normalized, no IDs), status code, region, environment, service version.
RED Method (for services)
Use RED to define the minimum set of metrics for any service:
- Rate: requests per second
rate(http_requests_total[5m]) - Errors: error rate as a fraction of total traffic
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) - Duration: latency at the 95th percentile
histogram_quantile(0.95, rate(request_duration_seconds_bucket[5m]))
USE Method (for resources)
Use USE to define metrics for infrastructure and resource-level components:
- Utilization: percentage of time the resource is busy (e.g., CPU%, disk I/O%)
- Saturation: work that is queued because it cannot be served immediately (e.g., run queue length)
- Errors: error events from the resource (e.g., disk I/O errors, network drops)
Code Examples
Python with prometheus_client:
from prometheus_client import Counter, Histogram, start_http_server
REQUEST_COUNT = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
REQUEST_DURATION = Histogram(
'request_duration_seconds',
'HTTP request duration',
['method', 'endpoint'],
buckets=[.005, .01, .025, .05, .1, .25, .5, 1, 2.5]
)
# Usage
with REQUEST_DURATION.labels(method='POST', endpoint='/payments').time():
result = process_payment()
REQUEST_COUNT.labels(method='POST', endpoint='/payments', status='200').inc()
# Expose metrics endpoint
start_http_server(8000)
TypeScript with prom-client:
import { Counter, Histogram, collectDefaultMetrics, register } from 'prom-client';
collectDefaultMetrics({ prefix: 'payment_service_' });
const requestCount = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'endpoint', 'status'],
});
const requestDuration = new Histogram({
name: 'request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'endpoint'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5],
});
// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
Go with prometheus/client_golang:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
requestCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
}, []string{"method", "endpoint", "status"})
requestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "request_duration_seconds",
Help: "HTTP request duration",
Buckets: prometheus.DefBuckets,
}, []string{"method", "endpoint"})
)
// Expose metrics endpoint
http.Handle("/metrics", promhttp.Handler())
Distributed Tracing with OpenTelemetry
Concepts
- Trace: the complete end-to-end journey of a single request, identified by a unique
trace_id - Span: a single named unit of work within a trace (has a start time, end time, status, and attributes); spans form a tree
- Context propagation: the mechanism for passing
trace_idandspan_idacross service boundaries via HTTP headers; use the W3Ctraceparentheader (standardized, supported everywhere) - Collector: a sidecar or standalone service (otel-collector) that receives span data, batches it, and forwards it to a backend (Jaeger, Tempo, etc.)
Auto-Instrumentation vs Manual Spans
- Auto-instrumentation: instrument HTTP frameworks, DB drivers, gRPC clients automatically — get traces with zero code changes (install the SDK + one setup file)
- Manual spans: wrap custom business logic to make it visible in traces (e.g., "processing payment", "calling fraud check API", "serializing response")
When in doubt: start with auto-instrumentation to get baseline visibility, then add manual spans where you need business context.
Setup (Python)
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# Bootstrap once at startup
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("payment-service")
# Manual span around business logic
with tracer.start_as_current_span("process_payment") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("payment.amount", amount)
span.set_attribute("payment.currency", currency)
result = charge_card(card_token, amount)
span.set_attribute("payment.result", result.status)
Setup (TypeScript)
import { trace, SpanStatusCode, context } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
// Bootstrap once at startup
const provider = new NodeTracerProvider();
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({ url: 'http://otel-collector:4317' })
)
);
provider.register();
const tracer = trace.getTracer('payment-service');
// Manual span with error handling
async function processPayment(orderId: string, amount: number) {
const span = tracer.startSpan('process_payment');
return context.with(trace.setSpan(context.active(), span), async () => {
span.setAttribute('order.id', orderId);
span.setAttribute('payment.amount', amount);
try {
const result = await chargeCard(orderId, amount);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message });
throw err;
} finally {
span.end();
}
});
}
Setup (Go)
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("payment-service"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
// Usage
tracer := otel.Tracer("payment-service")
ctx, span := tracer.Start(ctx, "process_payment")
defer span.End()
span.SetAttributes(
attribute.String("order.id", orderID),
attribute.Float64("payment.amount", amount),
)
Sampling Strategies
| Strategy | Description | Best for |
|---|---|---|
| Head sampling (always on) | Sample 100% of traces | Development, test environments |
| Head sampling (probabilistic) | Sample X% of traces at trace start | Steady-state production traffic cost control |
| Head sampling (never) | Drop all traces | When tracing a noisy low-value service |
| Tail sampling | Sample based on outcome (errors, slow spans) | Catch 100% of errors while sampling fast successes |
| Parent-based | Follow the upstream caller's sampling decision | Microservices (honor the decision made at the edge) |
Recommended production setup: parent-based at 10% for happy path + tail sampling to capture 100% of errors and traces with p99+ latency.
Alerting Design
Symptom-Based vs Cause-Based
Alert on symptoms (what users experience), not causes (what resources are doing):
| Type | Example | Why |
|---|---|---|
| Symptom (preferred) | "5xx error rate > 1% for 5 minutes" | Users are directly affected right now |
| Cause (avoid as primary) | "CPU usage > 80%" | CPU can spike without user impact; often too noisy |
| Cause (use for capacity) | "Disk > 85% full" | Predictive; gives time to act before impact |
Alert Design Rules
- Every alert must link to a runbook that explains diagnosis and remediation steps
severity: criticalmeans page someone immediately — it is happening now and users are affectedseverity: warningmeans investigate during business hours — degraded but not yet user-impacting- Avoid alerting on individual metrics; alert on SLO burn rate where possible
- Every alert should be actionable: if you cannot do anything about it, do not alert on it
- Add a
for:duration to avoid flapping on transient spikes (5 minutes is a safe default)
Prometheus Alerting Rule Example
groups:
- name: payment-service
rules:
- alert: HighErrorRate
expr: |
rate(http_requests_total{service="payment-service",status=~"5.."}[5m])
/ rate(http_requests_total{service="payment-service"}[5m]) > 0.01
for: 5m
labels:
severity: critical
service: payment-service
team: payments
annotations:
summary: "High error rate on payment-service"
description: "Error rate is {{ $value | humanizePercentage }} over last 5 minutes"
runbook: "https://wiki.example.com/runbooks/payment-service#high-error-rate"
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99,
rate(request_duration_seconds_bucket{service="payment-service"}[5m])
) > 2.0
for: 5m
labels:
severity: warning
service: payment-service
team: payments
annotations:
summary: "P99 latency above 2s on payment-service"
description: "P99 is {{ $value | humanizeDuration }}"
runbook: "https://wiki.example.com/runbooks/payment-service#high-latency"
SLOs and Error Budgets
Hierarchy
- SLI (Service Level Indicator): the specific metric being measured (e.g., fraction of HTTP requests returning 2xx, p99 latency)
- SLO (Service Level Objective): the internal target for an SLI (e.g., 99.9% of requests return 2xx over a rolling 30-day window)
- SLA (Service Level Agreement): the contractual commitment with defined penalties if breached; always set SLA below SLO to leave buffer
Writing a Good SLO
An SLO must specify:
- The SLI being measured
- The target percentage
- The time window (rolling 30 days is standard)
- What counts as "good" vs "bad" (the success criterion)
Examples:
- "99.9% of payment API requests return HTTP 2xx over a rolling 30-day window"
- "99% of payment API requests complete in under 500ms over a rolling 30-day window"
- "The payment service is reachable from all regions 99.95% of the time over 30 days"
Availability SLO Math
| SLO | Downtime per year | Downtime per month |
|---|---|---|
| 99% | 87.6 hours | 7.3 hours |
| 99.9% | 8.7 hours | 43.8 minutes |
| 99.95% | 4.4 hours | 21.9 minutes |
| 99.99% | 52.6 minutes | 4.4 minutes |
| 99.999% | 5.3 minutes | 26 seconds |
Most internal services target 99.9%. Customer-facing critical paths target 99.95%–99.99%. Do not set SLOs higher than you can actually achieve; an SLO you always exceed has no meaning.
Error Budget Policy
- Error budget = 1 − SLO (e.g., for 99.9% availability, 0.1% of requests can fail over 30 days)
- Track remaining error budget in a Grafana dashboard so teams see how much headroom they have
| Budget consumed | Action |
|---|---|
| < 50% | Normal — continue shipping |
| 50–75% | Review recent deployments; tighten testing |
| 75–99% | Freeze non-critical deployments; prioritize reliability work |
| 100% (exhausted) | All hands on reliability; no new features until budget recovers |
Burn Rate Alerting
Burn rate measures how fast you are spending your error budget relative to the expected pace.
- If SLO is 99.9% over 30 days, you have ~43.8 minutes of downtime budget per month
- A burn rate of 1.0 means you are consuming budget at exactly the sustainable rate
- A burn rate of 14x means you will exhaust your monthly budget in ~3 days
# 1-hour burn rate for a 99.9% SLO
(
1 - (
rate(http_requests_total{status=~"2.."}[1h])
/ rate(http_requests_total[1h])
)
) / (1 - 0.999)
Recommended burn rate alerts:
| Burn rate | Window | Severity | Action |
|---|---|---|---|
| > 14x | 1 hour | critical | Page immediately |
| > 6x | 6 hours | critical | Page immediately |
| > 3x | 1 day | warning | Ticket — investigate today |
| > 1x | 3 days | warning | Ticket — investigate this week |
Dashboard Design (Grafana)
Golden Signals Dashboard Template
Every service should have a single dashboard with at minimum four panels, one per golden signal:
-
Traffic — request rate (RPS) over time
sum(rate(http_requests_total{service="$service"}[5m])) by (endpoint) -
Latency — p50, p95, and p99 response time on one panel
histogram_quantile(0.99, sum(rate(request_duration_seconds_bucket{service="$service"}[5m])) by (le)) histogram_quantile(0.95, sum(rate(request_duration_seconds_bucket{service="$service"}[5m])) by (le)) histogram_quantile(0.50, sum(rate(request_duration_seconds_bucket{service="$service"}[5m])) by (le)) -
Errors — error rate as a percentage of total traffic
sum(rate(http_requests_total{service="$service",status=~"5.."}[5m])) / sum(rate(http_requests_total{service="$service"}[5m])) -
Saturation — CPU%, memory%, and connection pool usage on one panel
avg(container_cpu_usage_seconds_total{pod=~"$service.*"}) by (pod)
Useful Panel Types
| Type | Best for |
|---|---|
| Time series | Metrics over time — use for most panels |
| Stat | Single current value (uptime %, current error count) |
| Gauge | Percentage of a max (disk usage, pool utilization) |
| Table | Top N slow endpoints, recent error breakdown by status |
| Heatmap | Latency distribution from a histogram metric |
| Logs panel | Log lines from Loki shown alongside matching metrics |
| Bar chart | Comparison across services or endpoints at a point in time |
Dashboard Organization Tips
- Add a row per service tier (frontend, API, database, queue)
- Use template variables (
$service,$env,$region) so dashboards are reusable - Link each panel's title to the relevant runbook or alert rule
- Set the default time range to "Last 3 hours" with a 30-second auto-refresh for live debugging
- Pin the error budget remaining as a Stat panel at the top of every service dashboard
Red Flags
- High-cardinality label in a Prometheus metric — adding
user_id,order_id, or an un-normalized URL path as a label creates millions of time series and can crash Prometheus under memory pressure; only use low-cardinality labels - Alerting on CPU or memory utilization as a primary signal — CPU at 80% may have zero user impact while a queue backlog at 10% causes data loss; alert on symptoms (error rate, latency) and use resource metrics only for capacity forecasting
- Alert with no
for:duration — an alert that fires on the first data point trips on transient 1-second spikes; always addfor: 5mor similar to require the condition to be sustained before paging - Logging at DEBUG level in production — debug logs from a high-traffic service generate gigabytes per hour, burying actionable signals and inflating log storage costs; configure LOG_LEVEL=INFO in production
- Generating a new
request_idat each service hop — if each service creates its own ID, you cannot correlate log lines across service boundaries; generate once at the edge and propagate viaX-Request-IDor the W3Ctraceparentheader - Using
Summaryinstead ofHistogramfor latency metrics — summaries calculate quantiles client-side and cannot be aggregated across multiple instances in PromQL; histograms aggregate correctly across replicas - SLO defined as "99.9% uptime" without specifying the SLI or success criterion — "uptime" is ambiguous; a well-formed SLO specifies the SLI (fraction of HTTP 2xx responses), the target (99.9%), and the window (rolling 30 days)
- Span attributes containing PII (email, card number, user name) — trace data is often forwarded to third-party backends (Datadog, Honeycomb) with relaxed data retention; scrub or hash sensitive fields before attaching them to spans
Checklist
- All log lines include timestamp, level, service, trace_id, span_id, and request_id
- Log level is INFO in production; DEBUG only in development; never log sensitive data
- Correlation ID is generated at the edge and propagated through all downstream services and async queues
- RED metrics (rate, errors, duration) are instrumented on every service entry point
- Prometheus metric labels have low cardinality — no user IDs, order IDs, or raw URL paths
- Histogram buckets are tuned to the expected latency range of the service
- OpenTelemetry traces are configured with an OTLP exporter pointing at a collector
- Context propagation uses the W3C
traceparentheader across all HTTP and gRPC calls - Every alert has a
severitylabel, afor:duration, and arunbookannotation - SLOs are defined with an explicit SLI, target percentage, and rolling time window
- Error budget policy is documented: what the team does when 75%, 100% of budget is consumed
- Grafana dashboard includes all four golden signals (traffic, latency, errors, saturation) with template variables