Skip to main content

Path changes

GET /api/targets/{target_id}/path-changes

Observiant compares each new traceroute with the previous ones from the same agent to the same target. When the route changes in a way that matters, it records a path change with a before-and-after summary. Path changes exist only for ICMP targets, because only they have traceroutes.

Kinds of change​

kindRecorded when
as_path_changedThe sequence of autonomous systems changes and the new sequence appears in consecutive reports. A path that appears in a single report and then reverts, typical of ECMP load balancing, is not recorded.
became_unreachableThe destination stops answering: the previous report reached it and the new one does not.
became_reachableThe destination answers again after a report that did not reach it.
hops_changedThe hop addresses change and the destination is affected at the same time. Affected means latency moves by at least 5 ms and at least 20%, or packet loss at the destination goes from zero to non-zero or back.

One traceroute can produce more than one change, for example became_unreachable together with hops_changed. Hops that do not answer (???) count as missing information, not as a change. Hop changes without an effect on the destination are not recorded either, because they are usually load balancing or routers rate-limiting their ICMP replies. The first report after an agent restarts becomes the new baseline and does not produce a change.

Parameters​

ParameterDescription
agent_labelOptional. Only changes seen by this agent.
startOptional. Earliest change time, Unix milliseconds.
endOptional. Latest change time, Unix milliseconds.
limitOptional. Maximum number of changes, 1–500. Default and maximum 500.
resolve_namesOptional. true adds a hostnames map with the current reverse DNS name of each hop address in the response.

Changes are returned oldest first. Without start and end, the oldest changes in the retention period are returned. When you give both, the range can be at most 30 days.

Request​

END=$(date +%s000)
START=$(( END - 7 * 86400000 )) # last 7 days

curl --header "Authorization: Bearer $OBSERVIANT_API_KEY" \
"$OBSERVIANT_URL/api/targets/01M4MZ5TCN74PFRTXDKXYE4XWX/path-changes?start=$START&end=$END&resolve_names=true"

Response​

{
"changes": [
{
"target_id": "01M4MZ5TCN74PFRTXDKXYE4XWX",
"agent_label": "sys-amsterdam",
"display_name": "Amsterdam",
"timestamp": 1791702011845,
"kind": "as_path_changed",
"prev_as_path": ["AS64500", "AS64501", "AS15169"],
"new_as_path": ["AS64500", "AS64502", "AS15169"],
"prev_hop_hosts": ["192.0.2.1", "198.51.100.17", "198.51.100.66", "???", "203.0.113.9", "8.8.8.8"],
"new_hop_hosts": ["192.0.2.1", "198.51.100.17", "198.51.100.66", "203.0.113.130", "203.0.113.141", "???", "8.8.8.8"],
"prev_hop_asns": ["", "AS64500", "AS64500", "", "AS64501", "AS15169"],
"new_hop_asns": ["", "AS64500", "AS64500", "AS64502", "AS64502", "", "AS15169"],
"prev_hop_count": 6,
"new_hop_count": 7,
"destination_reached_from": true,
"destination_reached_to": true,
"prev_destination_latency_ms": 2.64,
"new_destination_latency_ms": 9.87,
"prev_destination_loss_pct": 0,
"new_destination_loss_pct": 20
}
],
"hostnames": {
"198.51.100.17": "ae1-10.ams-core1.example.net",
"198.51.100.66": "be20.ams-edge2.example.net",
"8.8.8.8": "dns.google"
}
}

Every change carries the route before (prev_…) and after (new_…) it, whatever its kind, so you can explain a change without fetching the traceroutes:

FieldDescription
target_id, agent_label, display_nameThe target and the agent whose traceroutes changed.
timestampStart time of the traceroute that confirmed the change, Unix milliseconds. Pass it to /mtr/nearest with the same agent_label to fetch that report.
kindOne of the kinds of change.
prev_as_path, new_as_pathAutonomous systems along the path, in order, without repeats.
prev_hop_hosts, new_hop_hostsHop addresses in path order, ??? for hops that did not answer.
prev_hop_asns, new_hop_asnsAutonomous system of each hop, aligned with the hop addresses. Empty strings where unknown.
prev_hop_count, new_hop_countNumber of hops.
destination_reached_from, destination_reached_toWhether the destination answered before and after.
prev_destination_latency_ms, new_destination_latency_msAverage round-trip time at the destination, milliseconds.
prev_destination_loss_pct, new_destination_loss_pctPacket loss at the destination, percent.
hostnamesOnly with resolve_names=true. Maps hop addresses to their reverse DNS names. Omitted when no names were found.

About hostnames:

  • Names are looked up when you make the request, so they are current names, not the names at the time of the change.
  • Lookups are limited to a few seconds per request. Addresses without a name, or whose lookup did not finish in time, are left out, so fall back to the address.
  • Requesting names makes the response slower. Ask for them only when you display the change to a person.

An unknown target, or a target without changes in the range, returns an empty changes array.

Fetch more changes​

A response holds at most 500 changes. If you receive exactly limit changes, repeat the request with start set to one millisecond after the last timestamp, and keep the same end.

Example: report recent route changes​

This script prints the route changes of the last 24 hours for every ICMP target:

route_changes.py
"""Print route changes of the last 24 hours for all ICMP targets."""
import os
import time

import requests

base_url = os.environ["OBSERVIANT_URL"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['OBSERVIANT_API_KEY']}"

end = int(time.time() * 1000)
start = end - 86400 * 1000

response = session.get(f"{base_url}/api/targets", timeout=30)
response.raise_for_status()
targets = [t for t in response.json() if t["protocol"] == "icmp"]

for target in targets:
response = session.get(
f"{base_url}/api/targets/{target['target_id']}/path-changes",
params={"start": start, "end": end},
timeout=30,
)
response.raise_for_status()
for change in response.json()["changes"]:
when = time.strftime("%Y-%m-%d %H:%M", time.gmtime(change["timestamp"] / 1000))
line = f"{when} UTC {target['label']} from {change['display_name']}: {change['kind']}"
if change["kind"] == "as_path_changed":
line += f" {' '.join(change['prev_as_path'])} -> {' '.join(change['new_as_path'])}"
print(line)
$ python route_changes.py
2026-10-11 07:00 UTC Google DNS from Amsterdam: as_path_changed AS64500 AS64501 AS15169 -> AS64500 AS64502 AS15169
2026-10-11 07:45 UTC Google DNS from Amsterdam: as_path_changed AS64500 AS64502 AS15169 -> AS64500 AS64501 AS15169