Skip to content
ExponaBLOG
PROMETHEUS, GRAFANA & MONITORING · DAY 090 / 100

Grafana annotations and links for Monitoring developers

Context on every graph.

Expona Admin6 min read

Introduction

By day 90 of your monitoring journey, you've built dashboards that surface the right metrics, configured alerts that wake you up when it matters, and tuned your data retention to balance cost and visibility. But dashboards that lack context turn investigation into archaeology—operators click around frantically looking for clues about what changed, where logs live, and whether that spike correlates with a deploy. Grafana annotations and links for monitoring developers solve this by embedding operational context directly into your visualizations, transforming dashboards from metric displays into incident investigation command centers.

Annotations for deploys and incidents

Annotations are events overlaid on time-series graphs. They mark moments in time: a deployment, a scaling event, a config change, or the start of an incident. When you're looking at a graph and see response time spike at 14:32, an annotation immediately tells you whether that's when the new version shipped.

The easiest integration is querying events from an external data source. Configure Grafana to read annotations from a Prometheus metrics endpoint, Loki logs, or a custom HTTP API. We typically use a simple HTTP endpoint that returns JSON events in Grafana's expected format.

yaml

POST /api/annotations
{
  "dashboardId": 42,
  "panelId": 7,
  "tags": ["deploy", "production"],
  "text": "Deployed service-api v2.14.3",
  "time": 1704067200000,
  "timeEnd": 1704067210000
}

In practice, we post annotations from our CI/CD pipeline immediately after a successful deployment. A webhook in your deployment system—whether that's ArgoCD, GitHub Actions, or a custom service—sends this event to Grafana's API. The annotation then appears as a vertical line or band on all relevant graphs, instantly showing teammates what changed and when.

For incident annotations, fire them when your alerting system detects a critical condition or when an on-call engineer manually escalates. This creates a visual record on your dashboards: you see the alert fire, the metrics begin to degrade, and the time you resolved it. Six months later, during a post-mortem, this timeline is invaluable.

yaml

curl -X POST http://grafana:3000/api/annotations \
  -H "Authorization: Bearer $GRAFANA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dashboardId": 42,
    "text": "P1 incident: Payment service latency exceeded 5s",
    "tags": ["incident", "p1", "payment"],
    "time": '$(date +%s000)',
    "isRegion": false
  }'

Use consistent tagging so you can filter annotations by type. A team working on microservices might tag by service name, environment, and severity. Grafana's annotation panel then lets operators quickly enable or disable categories—hide all non-incident annotations to reduce noise, or show only deploy events to correlate performance changes with releases.

Data links transform metric values into navigation: click a value on your graph and jump directly to related traces or logs in your observability stack. If you click a spike in error rate, a well-configured data link takes you to Jaeger or Tempo with a pre-filtered view of errors from that exact time window. No manual copying of timestamps or service names.

Grafana's data link feature lets you define URL templates that interpolate variables from your query and panel context. When you click a data point, Grafana constructs the destination URL using those values.

yaml

Data link URL template:
http://jaeger:16686/search?service=${service}&tags={"span.kind":"server","http.status_code":"500"}&start=${__from}&end=${__to}

Applied with variables:
- service from a label in your metric query
- __from and __to as Grafana's built-in time range variables

Set this up in the Grafana UI: edit a panel, go to Data links, add a new link, and write your template. Test it by clicking a value—the URL should construct correctly with actual values interpolated. Most teams link error rates to trace backends and request latencies to log aggregators.

The power emerges when you combine this with your dashboard's existing filters. If your dashboard is already filtered to a specific service and environment via template variables, those same variables carry through to your data links. Click a high error rate for auth-service in production, and you land in your trace backend already filtered to auth-service in production with errors from that time range.

As your system grows, you stop monitoring individual services and start monitoring the entire flow: how payment requests move through order processing, payment validation, fraud detection, and settlement. Dashboard links let you navigate this topology without context-switching to a service registry or wiki.

Create a "services" or "topology" dashboard that lists your critical services. Each entry is a dashboard link to that service's dedicated monitoring dashboard. Operators investigating a user-reported payment failure click through: service-api dashboard → order-service dashboard → payment-service dashboard, following the request flow with full context at each step.

Grafana's Dashboard link type in the panel editor lets you specify the target dashboard and pass variables through the URL. Use this to ensure context carries forward. If you're investigating a specific customer or time window, pass that context to the next dashboard.

yaml

Link configuration:
- Type: Dashboard
- Dashboard: order-service-metrics
- Parameters: customerId=${customerId}&timeRange=${__from}:${__to}

The linked dashboard then receives these parameters and auto-filters to the same customer and time range.

We've found it helps to create a "service map" dashboard that shows the architecture as boxes or swimlanes, with each box linking to that service's dashboard. When an incident affects multiple services, operators can quickly navigate the chain of dependencies and isolate where the problem actually originated.

Templated URLs with variables

Grafana's template variables—the dropdowns and filters at the top of dashboards—can be used in any link or annotation. This is where flexibility becomes powerful. Define a variable for environment, service, customer segment, or any dimension relevant to your domain, then use it in links to construct context-aware URLs.

Common variables include $environment, $service, $region, and time-based variables like $__from and $__to. Reference them in data links, dashboard links, and annotation queries.

yaml

Example: Loki logs data link for a panel showing request latency

URL template:
https://loki:3100/loki/api/v1/query_range?query={job="${service}",env="${environment}"}&start=${__from}&end=${__to}

When dashboard is filtered to service=auth-service, environment=production:
https://loki:3100/loki/api/v1/query_range?query={job="auth-service",env="production"}&start=1704067200&end=1704070800

This pattern scales well across many dashboards. Define variables once, use them everywhere. When an operator filters the dashboard to a specific service or environment, all links—data links, dashboard links, even annotation queries—automatically adjust to match. You've built a single source of truth for context.

Common mistakes

  • Hardcoding URLs without templating: Data links that don't use variables won't update when dashboard filters change. Always use ${variable} syntax or Grafana's built-in functions like $__from and $__to.
  • Annotations with no tags or inconsistent naming: If you tag deploys as "deploy", "deployment", and "released" across different integrations, filtering becomes a guessing game. Agree on tag naming conventions with your team and document them.
  • Breaking links when renaming dashboards or services: Hardcoded dashboard names in links become stale when you reorganize. Use Grafana's dashboard UID (a stable identifier) instead of the human-readable title, or store dashboard references in a configuration file rather than inside panels.
  • Ignoring time zone issues in annotations: Grafana expects milliseconds since epoch (UTC). If your CI/CD or event system sends timestamps in local time or seconds, annotations will appear at the wrong time. Standardize on UTC and milliseconds across all integrations.
  • Data links pointing to inaccessible systems: A link to an internal Jaeger instance won't help an engineer outside your network. Consider proxy URLs, VPN requirements, or multi-environment links (staging points to staging Jaeger, production points to production Jaeger).
  • Too many annotations cluttering the graph: Every event is worth recording, but not every annotation should display on every graph. Use tags to categorize and let operators choose what they see. A single graph with 20 overlaid annotations is harder to read than four separate panels, each with 5 annotations.

Key takeaways

  • Annotations overlay operational events (deploys, incidents, config changes) directly on your graphs, transforming raw metrics into a timeline of what changed and when.
  • Data links bridge metrics to logs and traces: click a spike in your graph and jump to pre-filtered trace or log views for that exact time window and service.
  • Dashboard links guide operators through your service topology, letting teams navigate from one dashboard to another without losing context or time range.
  • Grafana's template variables apply globally: filter once at the top of a dashboard and all links, annotations, and panels adjust automatically to the same context.
  • Standardize tagging conventions, use UTC timestamps in milliseconds, and leverage dashboard UIDs rather than names for stable references across environments and over time.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles