Skip to content
KONIGI

Dashboards / Visual representation / Service map

16 of 22

Service map

The viewer needs to know what depends on what, and which edge is unhealthy.

Updated September 10, 2026

Problem

Checkout is failing. Checkout calls six things, and two of those call four more, and the person on call has been here three weeks and does not have that graph in their head.

Solution

Nodes for services, edges for calls between them, both derived from traces rather than from a diagram somebody drew. Datadog states the ambition plainly: decompose the application into its component services and draw the observed dependencies in real time. Nodes are services as they appear in instrumentation; edges are aggregate calls from one service to another.

The word doing the work is observed. An architecture diagram in a wiki describes what someone intended eighteen months ago. A service map describes what actually made a request in the last few minutes, which is how teams discover the dependency nobody documented and the one everybody thought had been removed. Datadog’s version ages a service or connection out after thirty days without traces, so the map has a definition of “gone” as well as of “new”.

Health goes on the node. Datadog colours service node borders red or yellow from a consolidated health state pulled from anomalies, paging monitors and incidents. That consolidation is the design decision worth copying: a topology view where every node shows five metrics is unreadable, and one where each node shows a single derived state is scannable in a second and then drilled into.

Drawing the graph is easy. Laying it out is the hard part. Real service graphs are dense, cyclic and unbalanced, and generic force-directed layout produces a hairball that reorders itself on every refresh. A map a viewer cannot form a stable mental image of has failed regardless of how correct the edges are.

Use when

The estate is big enough that no one holds it in their head, calls cross team boundaries, and the question is “what is downstream of this” during an incident.

Don’t use when

There are eight services and everyone knows them. Also don’t use it as a monitoring surface: a topology view is a navigation and blast-radius tool, and a page that watches it continuously would be better off watching the four metrics that matter.

Trade-offs

Service maps only know what is instrumented, so an uninstrumented dependency is invisible in a view whose entire promise is completeness, which is worse than not having the view. Layout instability destroys recognition between visits. Aggregate edges hide direction of causality: a red edge tells you two services are having a bad time together and not which one started it. And at real scale the graph exceeds what a screen can carry, so every product has to pick a grouping, and the grouping determines what you are able to notice.

Checklist

  • Is the map derived from traces, or from something a human maintains?
  • What is the time window, and does an edge disappear when calls stop?
  • Does a node carry one derived state, or several competing metrics?
  • Is the layout stable between refreshes and between people?
  • Can the viewer see call volume and error rate on an edge, not just its existence?
  • What happens at a hundred services, and what grouping is available?
  • Can you get from a node to that service’s traces, logs and dashboards?
  • Are uninstrumented dependencies flagged, or silently absent?
  • Is direction of the call visible, and distinguishable from direction of blame?
  • Does a colourblind reader get the same health reading as everyone else?

Compare

Datadog builds the map automatically from APM traces, consolidates each service’s health into one border colour, and ages entries out after thirty days of silence, so the graph maintains itself in both directions. Jaeger ships a plainer dependency view derived from the same span parent-child relationships, which is enough to answer “who calls this” without pretending to be a monitoring surface. Grafana approaches it through Tempo’s service graph, assembled from span metrics, so the topology lives beside the dashboards rather than in a separate product. Netdata works at the host and container layer instead, mapping what is running where rather than what calls what, which answers a different question people often bring to the same screen.

Trace waterfall is one instance of what this view aggregates, and the natural drill target. Host map is the same spatial idea applied to infrastructure rather than to calls. Drill-down is what a node click has to do. Semantic status color decides what a red node means. Overview then detail is the structure this pattern sits at the top of.

Service map anatomy Nodes for services and edges for the calls between them, both derived from traces rather than from a diagram anyone drew. One node carries a derived health state on its border, one edge is unhealthy, and one dependency nobody documented is drawn because something actually called it. Observed, not drawn gateway checkout payments pricing tax postgres 1 2 3 1 THE UNHEALTHY EDGE An aggregate of calls from one service to another, so the edge can be sick while both nodes look fine on their own pages. 2 ONE DERIVED STATE Consolidated from anomalies, monitors and incidents. A node showing five metrics is a hairball; one showing a state is scannable in a second. 3 THE ONE NOBODY DREW A wiki diagram describes what somebody intended eighteen months ago. This describes what made a request four minutes ago. The map also needs a definition of "gone", or it accumulates every service that ever existed. Aging a node out after a month without traces is one. Drawing the graph is easy. Laying it out is not: real service graphs are dense, cyclic and unbalanced, and a map that reshuffles on refresh cannot be learned.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

A topology where the edges carry the interesting state. Node health is easy and mostly known; it is the dependency that is timing out which nobody can find in a list.

Tokens
--foreground--card--muted-foreground--border--status-warn--status-critical
gateway → checkout: 1180/min, 0.2% errorsgateway → pricing: 640/min, 0.1% errorscheckout → payments: 310/min, 17.0% errorscheckout → tax: 305/min, 0.0% errorspricing → postgres: 900/min, 0.0% errorspayments → postgres: 40/min, 0.0% errorsgatewaycheckout! paymentspricingtaxpostgres

ServiceMap.tsxNodes and edges from trace data, one derived state per node, an edge sick on its own error rate, new edges dashed, silent nodes aged off, positions stored so the layout holds.

import type { Status } from "../semantic-status-color/status";

/**
 * Observed, not drawn. A node is a service that produced traces, an edge is
 * calls between two of them, and both carry the two dates that give the map
 * a definition of "new" and of "gone". Positions are stored with the node,
 * because a map that reshuffles on refresh cannot be learned.
 */
export type Service = {
  id: string;
  x: number;
  y: number;
  /** One derived state per node. Five metrics on a node is a hairball. */
  status: Status;
  lastSeen: Date;
};

export type Call = {
  from: string;
  to: string;
  /** Per minute, over the window. Width follows it. */
  calls: number;
  errorRate: number;
  firstSeen: Date;
};

const DAY = 86_400_000;
/** An edge this young is drawn dashed: the dependency nobody had drawn. */
const NEW_FOR = 7 * DAY;
/** A node silent this long is gone, so the map does not accumulate history. */
const GONE_AFTER = 30 * DAY;
const UNHEALTHY = 0.05;

const NODE_H = 30;
const STROKE: Record<Status, string> = {
  nominal: "stroke-border", warn: "stroke-status-warn", critical: "stroke-status-critical", unknown: "stroke-muted-foreground",
};
const TEXT: Record<Status, string> = {
  nominal: "fill-foreground", warn: "fill-status-warn", critical: "fill-status-critical", unknown: "fill-muted-foreground",
};
/** A glyph beside the label, so the state survives greyscale. */
const GLYPH: Record<Status, string> = { nominal: "", warn: "▲ ", critical: "! ", unknown: "? " };

const width = (s: Service) => Math.max(56, s.id.length * 7 + 16);

/** Where a line from a to b leaves b's box, so the arrowhead sits on the edge. */
const clip = (a: Service, b: Service) => {
  const dx = b.x - a.x, dy = b.y - a.y;
  const t = Math.min((width(b) / 2) / Math.abs(dx || 1e-9), (NODE_H / 2) / Math.abs(dy || 1e-9));
  return { x: b.x - dx * t, y: b.y - dy * t };
};

export function ServiceMap({ services, calls, now, onSelect, width: w = 400, height = 250 }: {
  services: Service[];
  calls: Call[];
  /** The clock the window is measured from. */
  now: Date;
  onSelect: (id: string) => void;
  width?: number;
  height?: number;
}) {
  const live = services.filter((s) => now.getTime() - s.lastSeen.getTime() < GONE_AFTER);
  const byId = Object.fromEntries(live.map((s) => [s.id, s]));
  const edges = calls.filter((c) => byId[c.from] && byId[c.to]);
  const peak = Math.max(...edges.map((c) => c.calls));

  return (
    <div className="rounded-lg border bg-card p-4">
      <svg viewBox={`0 0 ${w} ${height}`} className="w-full" role="img" aria-label={`${live.length} services, ${edges.length} dependencies`}>
        <defs>
          <marker id="svc-arrow" viewBox="0 0 6 6" refX="5" refY="3" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
            <path d="M0 0 L6 3 L0 6 z" className="fill-muted-foreground" />
          </marker>
        </defs>
        {edges.map((c) => {
          const a = byId[c.from], b = byId[c.to];
          const end = clip(a, b);
          const sick = c.errorRate >= UNHEALTHY;
          const fresh = now.getTime() - c.firstSeen.getTime() < NEW_FOR;
          return (
            <line key={`${c.from}-${c.to}`} x1={a.x} y1={a.y} x2={end.x} y2={end.y}
              className={sick ? "stroke-status-critical" : fresh ? "stroke-muted-foreground" : "stroke-border"}
              strokeWidth={1.5 + 2 * (c.calls / peak)} strokeDasharray={fresh ? "5 4" : undefined} markerEnd="url(#svc-arrow)">
              <title>{`${c.from} → ${c.to}: ${c.calls}/min, ${(c.errorRate * 100).toFixed(1)}% errors`}</title>
            </line>
          );
        })}
        {live.map((s) => (
          <g key={s.id} role="button" tabIndex={0} onClick={() => onSelect(s.id)} onKeyDown={(e) => e.key === "Enter" && onSelect(s.id)} className="cursor-pointer">
            <rect x={s.x - width(s) / 2} y={s.y - NODE_H / 2} width={width(s)} height={NODE_H} rx="3"
              className={`fill-card ${STROKE[s.status]}`} strokeWidth={s.status === "nominal" ? 1.5 : 2} />
            <text x={s.x} y={s.y + 4} textAnchor="middle" className={`${TEXT[s.status]} text-[9px]`}>{GLYPH[s.status]}{s.id}</text>
          </g>
        ))}
      </svg>
    </div>
  );
}

demo.tsxHow it is called: six services, one failing edge, one dependency three days old, one node a month gone.

import { useState } from "react";
import { ServiceMap, type Call, type Service } from "./ServiceMap";

/**
 * Six services as traces saw them in the last few minutes. checkout to
 * payments is failing one call in six, so that edge is the sick one while
 * both nodes' own pages look fine; payments to postgres showed up three days
 * ago and nobody had drawn it. The clock is fixed so the ages are stable.
 */
const NOW = new Date("2026-09-15T09:00:00Z");
const ago = (days: number) => new Date(NOW.getTime() - days * 86_400_000);

const SERVICES: Service[] = [
  { id: "gateway",  x: 48,  y: 59,  status: "nominal",  lastSeen: NOW },
  { id: "checkout", x: 197, y: 47,  status: "nominal",  lastSeen: NOW },
  { id: "payments", x: 308, y: 47,  status: "critical", lastSeen: NOW },
  { id: "pricing",  x: 190, y: 127, status: "nominal",  lastSeen: NOW },
  { id: "tax",      x: 278, y: 127, status: "nominal",  lastSeen: NOW },
  { id: "postgres", x: 196, y: 205, status: "nominal",  lastSeen: NOW },
  // Retired in July. Still in the data, aged off the map.
  { id: "legacy-cart", x: 80, y: 205, status: "unknown", lastSeen: ago(62) },
];

const CALLS: Call[] = [
  { from: "gateway",  to: "checkout", calls: 1180, errorRate: 0.002, firstSeen: ago(400) },
  { from: "gateway",  to: "pricing",  calls: 640,  errorRate: 0.001, firstSeen: ago(400) },
  { from: "checkout", to: "payments", calls: 310,  errorRate: 0.17,  firstSeen: ago(400) },
  { from: "checkout", to: "tax",      calls: 305,  errorRate: 0.0,   firstSeen: ago(300) },
  { from: "pricing",  to: "postgres", calls: 900,  errorRate: 0.0,   firstSeen: ago(400) },
  { from: "payments", to: "postgres", calls: 40,   errorRate: 0.0,   firstSeen: ago(3) },
];

export default function Demo() {
  const [picked, setPicked] = useState<string | null>(null);
  return (
    <div>
      <ServiceMap services={SERVICES} calls={CALLS} now={NOW} onSelect={setPicked} />
      {picked && <p className="mt-2 text-xs text-muted-foreground">open traces, logs and dashboards for {picked}</p>}
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

Captures whose hotspots reference this pattern, grouped by product and dated. The dashed boxes are this pattern; hover any box for the note.

Grafana

The reference implementation for panel grids, template variables, and stat panels; most other tools are defined by how they differ from it.

Examples / Node graph panel September 10, 2026 Grafana Play (signed out; no version string exposed) sparse · dark · desktop-web
Seven services, and the panel is already too small to hold them. Three nodes are cut in half by the right edge and at least one more is somewhere past it, which is the force layout doing what force layouts do to a graph with more nodes than room. That is the hard part of this pattern and it is visible here at a scale of seven. Each node also carries four things at once: a number, a second number, and a ring split between a green arc and a red one. The legend names all four. A topology view where every node reports four measures is a view you read node by node, which is the opposite of what a map is for —one derived state per node is scannable in a second and drills into the rest. Here almost every ring is mostly red, so the channel that could have carried that state is saturated and distinguishes nothing.
  • Service map Nodes for services, edges for calls, both from instrumentation rather than from a diagram.
  • Semantic status color One node: two numbers inside and a success/error ring around it. Four measures, no verdict.
  • Legend and series toggle Four series named for a graph with seven nodes, which is the legend doing more work than the map.
  • Zoom and pan on time The only route to the nodes pushed off the right edge, and it doesn't move the rest of the page.