Build a Service Dependency Mapper
Map microservice dependencies and predict blast radius when services fail.
Introduction
30 Second Summary
When one service in a system goes down, the damage rarely stops there. It cascades. Like a single power station failing and blacking out an entire city grid, one broken microservice can silently take down everything that depends on it.
In this project, you will build a service dependency mapper that answers the question every SRE team needs answered: "If this service dies, what breaks?" You will create a Python API that models a microservices architecture, simulates cascading failures, and visualizes the blast radius in an interactive graph.
What You'll Build
A FastAPI backend that loads a service registry, computes criticality scores, and simulates cascading failures, paired with a browser-based interactive graph and a Prometheus + Grafana monitoring stack.
By the end of this project, you'll have:
- A Python FastAPI REST API with graph analysis endpoints that calculate criticality scores and simulate cascading failures across a 10-service e-commerce platform.
- An interactive dependency graph using vis-network.js where you click any service node to watch the failure cascade propagate in real-time.
- A full observability stack with Prometheus metrics, SLO-based alert rules, and a Grafana dashboard tracking RED metrics for the mapper API.
- Secret Mission: Add a "GameDay" chaos mode that randomly kills services on a timer and tracks Mean Time To Recovery.
Are there any prerequisites?
You need basic command line familiarity. Python experience is helpful but not required. The project teaches Python along the way.
You will need Docker, Python 3, VS Code, and a GitHub account on your machine. Step 1 walks you through verifying everything is ready.
Set Up the Project and GitHub Repository
Before you can build a dependency mapper, you need a clean project skeleton with the right tools installed. A solid foundation of folders, version control, and dependencies means less debugging later.
In this step, you'll create the directory structure, install the Python libraries that power the mapper, and connect everything to GitHub so your work is version-controlled from the start.
In this step, get ready to:
- Create the project folder with directories for the backend, frontend, and monitoring.
- Install Python dependencies in a virtual environment.
- Initialize a Git repository and push it to GitHub.
Create the project folder and directory structure
The project needs separate folders for the Python backend, the browser-based frontend, and the monitoring configuration files for Prometheus and Grafana.
- Open your terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder and move into it by running these commands:
mkdir dependency-mapper
cd dependency-mapper
- Create the full directory structure by running this command:
mkdir -p app static monitoring/grafana/provisioning/datasources
What do these folders hold?
- app/ holds the Python backend (FastAPI routes, graph logic, simulation engine).
- static/ holds the browser-based interactive graph visualization.
- monitoring/ holds Prometheus scrape configs, alert rules, and Grafana datasource provisioning.
- Confirm the directories were created by running:
ls
You should see app, monitoring, and static in the output.
- Open the project in VS Code by running:
code .
code command not found?
If your terminal says command not found: code, open VS Code manually, press Ctrl+Shift+P, type Shell Command: Install 'code' command in PATH, and select it. Then try the command again.
Help me set up the code command in my terminal.
Set up the Python virtual environment
A virtual environment keeps your project's Python packages isolated from your system. This prevents version conflicts between different projects on your machine.
- Create a virtual environment by running this command:
python3 -m venv venv
- Activate the virtual environment by running:
source venv/bin/activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran source venv/bin/activate (not just venv/bin/activate without source). Also confirm you are inside the dependency-mapper folder where you created the venv.
Help me activate my Python virtual environment.
Now you need a requirements.txt file that lists the exact package versions your project depends on. This guarantees anyone running your project installs the same versions you used.
- In VS Code, create a new file called requirements.txt in the root of your dependency-mapper folder.
- Paste the following contents into the file:
fastapi==0.136.1
uvicorn==0.34.0
prometheus-client==0.25.0
pyyaml==6.0.2
pydantic==2.11.1
What are these packages?
- fastapi is the web framework for your API endpoints.
- uvicorn is the server that runs FastAPI applications.
- prometheus-client lets you create and expose custom metrics that Prometheus scrapes.
- pyyaml parses YAML files (your service registry will be in YAML).
- pydantic handles request/response data validation for FastAPI.
- Save requirements.txt.
- Install all the packages by running this command in your terminal:
pip install -r requirements.txt
You should see output showing each package downloading and installing. The final line says Successfully installed followed by the package names.
Seeing permission errors or install failures?
Make sure your virtual environment is active (you should see (venv) in your prompt). If you accidentally ran pip install without the venv active, activate it first with source venv/bin/activate, then try again.
Help me fix my pip install errors.
Initialize Git and push to GitHub
Before making your first commit, you need a .gitignore file. This tells Git which files to exclude from version control. You do not want to commit your virtual environment or cache files.
- In VS Code, create a new file called .gitignore in the root of your dependency-mapper folder.
- Paste the following contents into the file:
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
.env
*.egg-info/
dist/
build/
.pytest_cache/
prometheus-data/
grafa na-data/
What does this .gitignore cover?
The first group ignores Python bytecode and compiled files. The venv/ and env/ lines exclude virtual environments.
The prometheus-data/ and grafana-data/ lines exclude Docker volume data that gets generated when you run the monitoring stack later.
- Save .gitignore.
- Initialize your Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit: project structure and dependencies"
You should see output confirming files were committed. Now push this to GitHub.
- Open GitHub in your browser.
- Click the + icon in the top right corner, then select New repository.
- Enter dependency-mapper as the repository name.
- Leave all other settings at their defaults (Public, no README, no .gitignore, no license).
- Click Create repository.
GitHub shows a quick setup page with commands. You already have a local repo, so use the "push an existing repository" commands.
- Connect your local repo to GitHub and push by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/dependency-mapper.git"]]
git branch -M main
git push -u origin main
- Refresh your GitHub repository page in the browser.
You should see your .gitignore and requirements.txt files listed in the repository.
Push failed or asking for credentials?
If Git asks for a username and password, GitHub no longer accepts passwords for HTTPS. You need a Personal Access Token instead. Go to Settings > Developer settings > Personal access tokens > Tokens (classic) on GitHub, generate a token with repo scope, and use that token as your password.
Help me push my code to GitHub.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved in VS Code and your GitHub repo shows the commit.
ⓧ I'd like to double check the full code
Here are both files as they should look at the end of this step.
fastapi==0.136.1
uvicorn==0.34.0
prometheus-client==0.25.0
pyyaml==6.0.2
pydantic==2.11.1
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
.env
*.egg-info/
dist/
build/
.pytest_cache/
prometheus-data/
grafana-data/
Your project skeleton is ready and backed up on GitHub. Next up, you'll define the service registry that describes a realistic e-commerce platform with 10 microservices and their dependencies.
Define the Service Registry
Your project is set up with all the dependencies installed. Now it is time to define the system you are building a tool to analyze.
Every microservices platform has a catalog of services describing what each one does, who owns it, and what it depends on. In this step, you will create that catalog as a YAML file and write a Python module that parses it into a graph structure your simulator can reason about.
In this step, get ready to:
- Create a YAML service registry defining 10 microservices with dependencies, owners, and SLO tiers.
- Write a Python module that parses the YAML into a directed graph data structure.
- Build failure simulation functions that propagate outages through the graph.
Create the service registry
Real tools like Backstage and PagerDuty use YAML-based service catalogs that live in version control. Your registry defines a realistic e-commerce platform with 10 microservices.
- Create a new file called services.yaml in the root of your dependency-mapper folder by pasting the following:
services:
api-gateway:
owner: platform-team
slo_tier: critical
depends_on: []
failure_probability: 1.0
user_impact_weight: 1.0
description: "Entry point for all client requests"
user-service:
owner: identity-team
slo_tier: high
depends_on:
- api-gateway
failure_probability: 0.9
user_impact_weight: 0.7
description: "Authentication and user profiles"
order-service:
owner: commerce-team
slo_tier: critical
depends_on:
- api-gateway
- user-service
- inventory-service
- payment-service
failure_probability: 0.95
user_impact_weight: 0.9
description: "Order creation and management"
payment-service:
owner: payments-team
slo_tier: critical
depends_on:
- api-gateway
failure_probability: 1.0
user_impact_weight: 0.95
description: "Payment processing and billing"
inventory-service:
owner: commerce-team
slo_tier: high
depends_on:
- api-gateway
failure_probability: 0.8
user_impact_weight: 0.6
description: "Stock levels and warehouse management"
notification-service:
owner: platform-team
slo_tier: medium
depends_on:
- order-service
- user-service
failure_probability: 0.7
user_impact_weight: 0.3
description: "Email, SMS, and push notifications"
search-service:
owner: discovery-team
slo_tier: high
depends_on:
- api-gateway
- inventory-service
failure_probability: 0.85
user_impact_weight: 0.5
description: "Product search and filtering"
recommendation-service:
owner: discovery-team
slo_tier: medium
depends_on:
- user-service
- inventory-service
- analytics-service
failure_probability: 0.6
user_impact_weight: 0.2
description: "Personalized product recommendations"
shipping-service:
owner: logistics-team
slo_tier: high
depends_on:
- order-service
failure_probability: 0.9
user_impact_weight: 0.7
description: "Shipping rate calculation and tracking"
analytics-service:
owner: data-team
slo_tier: medium
depends_on:
- api-gateway
failure_probability: 0.5
user_impact_weight: 0.1
description: "Event tracking and business analytics"
What do these fields mean?
- owner identifies which team is responsible for the service.
- slo_tier (critical, high, medium) defines how much uptime is promised for this service.
- depends_on lists which other services this one needs to function.
- failure_probability controls how likely this service is to fail when a dependency goes down. A value of 1.0 means it always fails. A lower value models graceful degradation.
- user_impact_weight rates how much end users feel this service going down. The api-gateway has a weight of 1.0 because every user request flows through it.
- Save services.yaml.
- Verify the YAML is valid by running this command in your terminal:
python -c "import yaml; data = yaml.safe_load(open('services.yaml')); print(f'{len(data["services"])} services loaded')"
You should see 10 services loaded printed in your terminal.
Seeing a YAML parsing error?
- YAML is whitespace-sensitive. Make sure each indentation level uses exactly 2 spaces, not tabs.
- Check that every depends_on list item starts with a dash and a space (- api-gateway).
- Make sure your virtual environment is activated. You should see (venv) in your terminal prompt.
Help me debug my YAML parsing error.
Parse the registry into a graph
The YAML file is just data on disk. Your Python code needs to load it and build a reverse dependency graph that answers the question: if service X dies, which services depend on X?
- Create an empty file called __init__.py inside your app/ folder by running:
touch app/__init__.py
Why an empty __init__.py?
This file marks the app/ directory as a Python package. Without it, Python cannot import modules from that folder.
- Create a new file called registry.py inside the app/ folder.
- Add the imports and the load_registry function by pasting this code:
import yaml
from pathlib import Path
from collections import deque
def load_registry(path: str = "services.yaml") -> dict:
"""Load the service registry from a YAML file."""
file_path = Path(path)
with open(file_path, "r") as f:
data = yaml.safe_load(f)
return data["services"]
What does this code do?
- yaml is the PyYAML library for parsing YAML files. Path provides cross-platform file path handling. deque is a double-ended queue you will use for breadth-first traversal later.
- load_registry() opens the YAML file, parses it, and returns just the services dictionary containing all 10 microservices.
- Next, add the build_reverse_graph function below load_registry in the same file:
def build_reverse_graph(services: dict) -> dict:
"""Build a reverse dependency graph.
For each service, find all services that depend on it.
If service A depends_on service B, then B's reverse entry includes A.
"""
reverse = {name: [] for name in services}
for name, config in services.items():
for dependency in config.get("depends_on", []):
if dependency in reverse:
reverse[dependency].append(name)
return reverse
Why a reverse graph?
The YAML says "order-service depends on payment-service." But for blast radius you need the opposite direction: "if payment-service dies, what breaks?" The reverse graph flips every edge so you can traverse forward from any failed service to find everything downstream that breaks.
- Save app/registry.py.
- Test the two functions by running:
python -c "from app.registry import load_registry, build_reverse_graph; s = load_registry(); r = build_reverse_graph(s); print(f'api-gateway dependents: {r[\"api-gateway\"]}')"
You should see a list of services that depend on api-gateway, such as user-service, order-service, and payment-service.
Seeing ModuleNotFoundError?
- Make sure you are running the command from the root dependency-mapper folder (where services.yaml lives), not from inside the app/ folder.
- Confirm app/__init__.py exists. Without it, Python cannot import from the app package.
Help me fix my Python import error for app.registry.
Build the failure simulation functions
Now you need the logic that actually answers the blast radius question. These functions traverse the reverse graph to find every service that would break and compute the overall user impact.
- In app/registry.py, add the calculate_criticality_scores function below build_reverse_graph:
def calculate_criticality_scores(services: dict) -> dict:
"""Calculate criticality score for each service.
Score = number of transitively affected services * user_impact_weight
A service with many downstream dependents and high user impact is more critical.
"""
reverse_graph = build_reverse_graph(services)
scores = {}
for service_name in services:
affected = get_affected_services(service_name, reverse_graph, services)
total_impact = 0.0
for affected_name in affected:
weight = services[affected_name].get("user_impact_weight", 0.5)
total_impact += weight
own_weight = services[service_name].get("user_impact_weight", 0.5)
scores[service_name] = round(total_impact + own_weight, 2)
return scores
What does this code do?
For each service, this function asks: "How bad would it be if this service died?" It sums up the user_impact_weight of every transitively affected service plus its own weight. A higher score means bigger blast radius potential.
This function calls get_affected_services which you will add next.
- Add the get_affected_services function below calculate_criticality_scores:
def get_affected_services(
failed_service: str, reverse_graph: dict, services: dict
) -> list:
"""Get all services transitively affected by a service going down.
Uses breadth-first traversal on the reverse dependency graph.
Respects failure_probability on edges.
"""
affected = []
visited = {failed_service}
queue = deque([failed_service])
while queue:
current = queue.popleft()
dependents = reverse_graph.get(current, [])
for dependent in dependents:
if dependent not in visited:
prob = services[dependent].get("failure_probability", 1.0)
if prob >= 0.5:
visited.add(dependent)
affected.append(dependent)
queue.append(dependent)
return affected
What does this code do?
This is the core graph traversal. Starting from the failed service, it uses breadth-first search (BFS) to visit every service that depends on it, then every service that depends on those, and so on.
The failure_probability check models real-world behavior. Not every dependency is a hard dependency. A service with low failure_probability (like 0.3) gracefully degrades instead of crashing, so the failure stops propagating through it.
- Add the final function, simulate_failure, below get_affected_services:
def simulate_failure(service_name: str, services: dict) -> dict:
"""Simulate a service failure and return the blast radius."""
reverse_graph = build_reverse_graph(services)
affected = get_affected_services(service_name, reverse_graph, services)
total_user_impact = services[service_name].get("user_impact_weight", 0.5)
for name in affected:
total_user_impact += services[name].get("user_impact_weight", 0.5)
total_possible_impact = sum(
s.get("user_impact_weight", 0.5) for s in services.values()
)
impact_percentage = round((total_user_impact / total_possible_impact) * 100, 1)
affected_teams = set()
affected_teams.add(services[service_name]["owner"])
for name in affected:
affected_teams.add(services[name]["owner"])
return {
"failed_service": service_name,
"affected_services": affected,
"affected_count": len(affected),
"user_impact_percentage": impact_percentage,
"affected_teams": sorted(list(affected_teams)),
"propagation_path": [service_name] + affected,
}
What does this code do?
- It builds the reverse graph, then calls get_affected_services to find every service that would break.
- It sums up user_impact_weight values and divides by the total possible impact to produce a percentage. If payment-service dies and takes out 60% of impact weight, the user impact is 60%.
- It collects the owning teams so incident responders know who to page.
- Save app/registry.py.
- Test the full simulation by running:
python -c "from app.registry import load_registry, simulate_failure; s = load_registry(); r = simulate_failure('payment-service', s); print(f'Blast radius: {r[\"affected_count\"]} services, {r[\"user_impact_percentage\"]}% user impact')"
You should see output showing that killing payment-service takes down multiple downstream services with a significant user impact percentage.
Seeing a NameError or unexpected results?
- If you see NameError: name 'get_affected_services' is not defined, check that all five functions are in the correct order in your file: load_registry, build_reverse_graph, calculate_criticality_scores, get_affected_services, simulate_failure.
- If the affected count is 0, double check that your services.yaml has the correct depends_on entries for order-service (it should list payment-service as a dependency).
Help me debug my simulate_failure function output.
✔️ Awesome, I've got everything!
Great. Double check you have saved both services.yaml and app/registry.py.
ⓧ I'd like to double check the full code
Here is the complete app/registry.py file:
import yaml
from pathlib import Path
from collections import deque
def load_registry(path: str = "services.yaml") -> dict:
"""Load the service registry from a YAML file."""
file_path = Path(path)
with open(file_path, "r") as f:
data = yaml.safe_load(f)
return data["services"]
def build_reverse_graph(services: dict) -> dict:
"""Build a reverse dependency graph.
For each service, find all services that depend on it.
If service A depends_on service B, then B's reverse entry includes A.
"""
reverse = {name: [] for name in services}
for name, config in services.items():
for dependency in config.get("depends_on", []):
if dependency in reverse:
reverse[dependency].append(name)
return reverse
def calculate_criticality_scores(services: dict) -> dict:
"""Calculate criticality score for each service.
Score = number of transitively affected services * user_impact_weight
A service with many downstream dependents and high user impact is more critical.
"""
reverse_graph = build_reverse_graph(services)
scores = {}
for service_name in services:
affected = get_affected_services(service_name, reverse_graph, services)
total_impact = 0.0
for affected_name in affected:
weight = services[affected_name].get("user_impact_weight", 0.5)
total_impact += weight
own_weight = services[service_name].get("user_impact_weight", 0.5)
scores[service_name] = round(total_impact + own_weight, 2)
return scores
def get_affected_services(
failed_service: str, reverse_graph: dict, services: dict
) -> list:
"""Get all services transitively affected by a service going down.
Uses breadth-first traversal on the reverse dependency graph.
Respects failure_probability on edges.
"""
affected = []
visited = {failed_service}
queue = deque([failed_service])
while queue:
current = queue.popleft()
dependents = reverse_graph.get(current, [])
for dependent in dependents:
if dependent not in visited:
prob = services[dependent].get("failure_probability", 1.0)
if prob >= 0.5:
visited.add(dependent)
affected.append(dependent)
queue.append(dependent)
return affected
def simulate_failure(service_name: str, services: dict) -> dict:
"""Simulate a service failure and return the blast radius."""
reverse_graph = build_reverse_graph(services)
affected = get_affected_services(service_name, reverse_graph, services)
total_user_impact = services[service_name].get("user_impact_weight", 0.5)
for name in affected:
total_user_impact += services[name].get("user_impact_weight", 0.5)
total_possible_impact = sum(
s.get("user_impact_weight", 0.5) for s in services.values()
)
impact_percentage = round((total_user_impact / total_possible_impact) * 100, 1)
affected_teams = set()
affected_teams.add(services[service_name]["owner"])
for name in affected:
affected_teams.add(services[name]["owner"])
return {
"failed_service": service_name,
"affected_services": affected,
"affected_count": len(affected),
"user_impact_percentage": impact_percentage,
"affected_teams": sorted(list(affected_teams)),
"propagation_path": [service_name] + affected,
}
Your service registry is defined and your simulation engine can already predict blast radius from the command line. Next up, you will build a scoring engine that ranks every service by how critical it is to protect.
Build the FastAPI Backend
Your registry module can load services and simulate failures, but it lives entirely in Python memory. No other system can interact with it yet.
In this step, you will wrap the registry logic in a FastAPI web server that exposes REST endpoints. You will also wire in Prometheus metrics from the start so the app is observable from day one.
In this step, get ready to:
- Set up a FastAPI app with Prometheus metrics instrumentation.
- Create REST endpoints for listing services, simulating failures, and resetting state.
- Run the API locally and test a blast radius simulation.
Set up the FastAPI app with Prometheus metrics
Every production API needs observability built in from the start. You will define four Prometheus metric types that track request counts, latency, simulation runs, and service health states.
- Create a static directory in your project root. The app will serve frontend files from here in a later step.
- Create the directory by running this command:
mkdir static
- Create a new file called main.py inside the app/ folder.
- Add the following imports, app setup, and Prometheus metric definitions:
import time
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from prometheus_client import Counter, Histogram, Gauge, make_asgi_app
from pydantic import BaseModel
from app.registry import (
load_registry,
calculate_criticality_scores,
simulate_failure,
)
app = FastAPI(title="Service Dependency Mapper", version="1.0.0")
# Prometheus metrics
REQUEST_COUNT = Counter(
"mapper_requests_total",
"Total requests to the mapper API",
["method", "endpoint"],
)
REQUEST_LATENCY = Histogram(
"mapper_request_duration_seconds",
"Request latency in seconds",
["endpoint"],
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
)
SIMULATION_COUNT = Counter(
"mapper_simulations_total",
"Total failure simulations run",
["target_service"],
)
SERVICE_HEALTH = Gauge(
"mapper_service_health",
"Current health state of each service (1=healthy, 0=down)",
["service"],
)
# Mount Prometheus metrics endpoint
metrics_app = make_asgi_app()
app.mount("/metrics", metrics_app)
# Mount static files for the frontend
app.mount("/static", StaticFiles(directory="static"), name="static")
# Load service registry at startup
services = load_registry("services.yaml")
criticality_scores = calculate_criticality_scores(services)
# Initialize health gauges
for service_name in services:
SERVICE_HEALTH.labels(service=service_name).set(1)
What does this code do?
This setup block handles four things:
- It imports the registry functions you built in the previous step so the API can call them.
- It defines four Prometheus metrics: a Counter for total requests, a Histogram for latency, a Counter for simulations, and a Gauge for live service health.
- It mounts the Prometheus metrics endpoint at /metrics so Prometheus can scrape the app later.
- It loads the service registry once at startup and pre-computes criticality scores so every request is fast.
What is a Counter vs a Histogram vs a Gauge?
A Counter only goes up. It tracks totals like "how many requests have we served." A Histogram records the distribution of values (like response times) into configurable buckets. A Gauge can go up or down, making it perfect for "current state" values like whether a service is healthy right now.
You will not see a change yet. The next chunk adds the endpoint functions that make this app functional.
Create the API endpoints
The app needs endpoints that external systems and the frontend can call. Each endpoint will increment the relevant Prometheus metrics so you get observability for free.
- In app/main.py, add the following code below the health gauge initialization loop:
class SimulateRequest(BaseModel):
service: str
@app.get("/")
async def serve_frontend():
"""Serve the main frontend page."""
return FileResponse("static/index.html")
@app.get("/api/services")
async def get_services():
"""Return all services with criticality scores and metadata."""
start = time.time()
REQUEST_COUNT.labels(method="GET", endpoint="/api/services").inc()
result = []
for name, config in services.items():
result.append(
{
"name": name,
"owner": config["owner"],
"slo_tier": config["slo_tier"],
"depends_on": config.get("depends_on", []),
"criticality_score": criticality_scores.get(name, 0),
"user_impact_weight": config.get("user_impact_weight", 0.5),
"description": config.get("description", ""),
}
)
duration = time.time() - start
REQUEST_LATENCY.labels(endpoint="/api/services").observe(duration)
return {"services": result}
@app.post("/api/simulate")
async def run_simulation(request: SimulateRequest):
"""Simulate a service failure and return the blast radius."""
start = time.time()
REQUEST_COUNT.labels(method="POST", endpoint="/api/simulate").inc()
if request.service not in services:
raise HTTPException(
status_code=404,
detail=f"Service '{request.service}' not found in registry",
)
SIMULATION_COUNT.labels(target_service=request.service).inc()
result = simulate_failure(request.service, services)
# Update health gauges to reflect simulation
SERVICE_HEALTH.labels(service=request.service).set(0)
for affected in result["affected_services"]:
SERVICE_HEALTH.labels(service=affected).set(0)
duration = time.time() - start
REQUEST_LATENCY.labels(endpoint="/api/simulate").observe(duration)
return result
@app.post("/api/reset")
async def reset_simulation():
"""Reset all services back to healthy state."""
REQUEST_COUNT.labels(method="POST", endpoint="/api/reset").inc()
for service_name in services:
SERVICE_HEALTH.labels(service=service_name).set(1)
return {"status": "all services reset to healthy"}
@app.get("/api/health")
async def health_check():
"""Health check endpoint for Prometheus."""
return {"status": "healthy"}
What do these endpoints do?
- GET /api/services returns every service with its criticality score, owner, SLO tier, and dependencies.
- POST /api/simulate accepts a service name, runs the cascading failure simulation, and returns the blast radius (affected services, user impact percentage, affected teams).
- POST /api/reset sets all service health gauges back to 1 (healthy) so you can run another simulation cleanly.
- GET /api/health is a simple health check that Prometheus will use to confirm the app is alive.
- Save app/main.py.
Seeing a red underline on the imports?
Make sure your virtual environment is activated. VS Code may not find the installed packages otherwise. Check that your terminal prompt shows the venv name in parentheses.
Help me fix import errors in my FastAPI project
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
import time
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from prometheus_client import Counter, Histogram, Gauge, make_asgi_app
from pydantic import BaseModel
from app.registry import (
load_registry,
calculate_criticality_scores,
simulate_failure,
)
app = FastAPI(title="Service Dependency Mapper", version="1.0.0")
# Prometheus metrics
REQUEST_COUNT = Counter(
"mapper_requests_total",
"Total requests to the mapper API",
["method", "endpoint"],
)
REQUEST_LATENCY = Histogram(
"mapper_request_duration_seconds",
"Request latency in seconds",
["endpoint"],
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
)
SIMULATION_COUNT = Counter(
"mapper_simulations_total",
"Total failure simulations run",
["target_service"],
)
SERVICE_HEALTH = Gauge(
"mapper_service_health",
"Current health state of each service (1=healthy, 0=down)",
["service"],
)
# Mount Prometheus metrics endpoint
metrics_app = make_asgi_app()
app.mount("/metrics", metrics_app)
# Mount static files for the frontend
app.mount("/static", StaticFiles(directory="static"), name="static")
# Load service registry at startup
services = load_registry("services.yaml")
criticality_scores = calculate_criticality_scores(services)
# Initialize health gauges
for service_name in services:
SERVICE_HEALTH.labels(service=service_name).set(1)
class SimulateRequest(BaseModel):
service: str
@app.get("/")
async def serve_frontend():
"""Serve the main frontend page."""
return FileResponse("static/index.html")
@app.get("/api/services")
async def get_services():
"""Return all services with criticality scores and metadata."""
start = time.time()
REQUEST_COUNT.labels(method="GET", endpoint="/api/services").inc()
result = []
for name, config in services.items():
result.append(
{
"name": name,
"owner": config["owner"],
"slo_tier": config["slo_tier"],
"depends_on": config.get("depends_on", []),
"criticality_score": criticality_scores.get(name, 0),
"user_impact_weight": config.get("user_impact_weight", 0.5),
"description": config.get("description", ""),
}
)
duration = time.time() - start
REQUEST_LATENCY.labels(endpoint="/api/services").observe(duration)
return {"services": result}
@app.post("/api/simulate")
async def run_simulation(request: SimulateRequest):
"""Simulate a service failure and return the blast radius."""
start = time.time()
REQUEST_COUNT.labels(method="POST", endpoint="/api/simulate").inc()
if request.service not in services:
raise HTTPException(
status_code=404,
detail=f"Service '{request.service}' not found in registry",
)
SIMULATION_COUNT.labels(target_service=request.service).inc()
result = simulate_failure(request.service, services)
# Update health gauges to reflect simulation
SERVICE_HEALTH.labels(service=request.service).set(0)
for affected in result["affected_services"]:
SERVICE_HEALTH.labels(service=affected).set(0)
duration = time.time() - start
REQUEST_LATENCY.labels(endpoint="/api/simulate").observe(duration)
return result
@app.post("/api/reset")
async def reset_simulation():
"""Reset all services back to healthy state."""
REQUEST_COUNT.labels(method="POST", endpoint="/api/reset").inc()
for service_name in services:
SERVICE_HEALTH.labels(service=service_name).set(1)
return {"status": "all services reset to healthy"}
@app.get("/api/health")
async def health_check():
"""Health check endpoint for Prometheus."""
return {"status": "healthy"}
Run and test the API locally
Time to start the server and confirm the endpoints respond. FastAPI ships with uvicorn as its ASGI server, and the --reload flag auto-restarts on file changes.
- Start the FastAPI server by running this command:
uvicorn app.main:app --reload
What does this command do?
uvicorn is an ASGI server that runs your FastAPI app. app.main:app tells it to look in the app/main.py file and find the variable named app. The --reload flag watches for file changes and restarts automatically.
- You should see output like INFO: Uvicorn running on http://127.0.0.1:8000 in your terminal.
Seeing ModuleNotFoundError?
Make sure you are running the command from your project root directory (the folder that contains the app/ folder and services.yaml). Also confirm your virtual environment is active.
Help me fix ModuleNotFoundError when running uvicorn
- Open a second terminal window (keep the server running in the first one).
- Test the services endpoint by running this command:
curl http://localhost:8000/api/services
- You should see a JSON response listing all 10 services with their criticality scores, owners, and SLO tiers.
- Now test the blast radius simulation by running this command:
curl -X POST http://localhost:8000/api/simulate -H "Content-Type: application/json" -d '{"service": "payment-service"}'
- You should see a JSON response showing the blast radius: affected services, user impact percentage, and affected teams.
What should the simulation response look like?
The response shows that killing payment-service cascades into order-service (which depends on it), then further into shipping-service and notification-service. The user impact percentage reflects how much of the platform's total weight is affected.
Getting Connection refused?
Make sure the uvicorn server is still running in your first terminal. If you accidentally closed it, run uvicorn app.main:app --reload again from your project root.
Help me troubleshoot my FastAPI server connection
Your API is live and responding with real blast radius data. Next up, you will build an interactive frontend that visualizes this dependency graph and lets you click any node to trigger a failure simulation.
Build the Interactive Frontend
Your backend has all the intelligence it needs to calculate criticality scores and simulate cascading failures. The problem is that JSON responses in a terminal are hard to reason about when you are dealing with 10 interconnected services.
In this step, you will build a browser-based interactive graph that makes dependency relationships visible at a glance. Clicking any service node triggers a live failure simulation, coloring the blast radius red so you can immediately see the impact.
In this step, get ready to:
- Create an interactive dependency graph using vis-network.js loaded from a CDN.
- Add click-to-kill interaction that triggers cascading failure simulations.
- Display a blast radius summary panel with affected services, user impact, and team ownership.
Create the HTML page with vis-network.js
The frontend is a single HTML file that loads vis-network.js from a CDN. This library is purpose-built for rendering interactive network graphs in the browser with zero build tools required.
- Create a static folder inside your project directory by running this command:
mkdir static
- Create a new file called index.html inside the static folder.
- Paste the following code into static/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Dependency Mapper</title>
<!-- Load vis-network from CDN for interactive graph rendering -->
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; display: flex; height: 100vh; }
#sidebar { width: 350px; padding: 20px; background: #16213e; overflow-y: auto; border-right: 1px solid #0f3460; }
#sidebar h1 { font-size: 1.3rem; margin-bottom: 10px; color: #00d4ff; }
#sidebar p { font-size: 0.85rem; color: #aaa; margin-bottom: 20px; }
#graph-container { flex: 1; }
#mynetwork { width: 100%; height: 100%; }
.panel { background: #0f3460; border-radius: 8px; padding: 15px; margin-bottom: 15px; }
.panel h3 { font-size: 0.95rem; margin-bottom: 8px; color: #00d4ff; }
.stat { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid #1a1a2e; font-size: 0.85rem; }
.stat-value { font-weight: bold; color: #00d4ff; }
.impact-high { color: #ff4757; }
.impact-medium { color: #ffa502; }
.impact-low { color: #2ed573; }
.affected-list { list-style: none; padding: 0; }
.affected-list li { padding: 4px 8px; margin: 3px 0; background: #1a1a2e; border-radius: 4px; font-size: 0.8rem; }
.btn { width: 100%; padding: 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; margin-top: 10px; }
.btn-reset { background: #2ed573; color: #1a1a2e; font-weight: bold; }
.btn-reset:hover { background: #26bf65; }
#instructions { font-size: 0.8rem; color: #888; margin-top: 15px; }
</style>
</head>
<body>
<div id="sidebar">
<h1>Service Dependency Mapper</h1>
<p>Click any service node to simulate a failure and see the blast radius.</p>
<!-- Blast radius panel, hidden until a simulation runs -->
<div class="panel" id="blast-panel" style="display:none;">
<h3>Blast Radius</h3>
<div class="stat"><span>Failed Service</span><span class="stat-value" id="failed-service">-</span></div>
<div class="stat"><span>Affected Services</span><span class="stat-value" id="affected-count">-</span></div>
<div class="stat"><span>User Impact</span><span class="stat-value" id="user-impact">-</span></div>
<div class="stat"><span>Teams Affected</span><span class="stat-value" id="teams-affected">-</span></div>
<h3 style="margin-top: 12px;">Affected Services</h3>
<ul class="affected-list" id="affected-list"></ul>
</div>
<!-- Service info panel -->
<div class="panel" id="service-panel">
<h3>Service Info</h3>
<p id="service-info-text" style="font-size: 0.85rem; color: #aaa;">Select a service to see details</p>
</div>
<button class="btn btn-reset" onclick="resetSimulation()">Reset All Services</button>
<p id="instructions">Nodes are colored by criticality: green (low) to red (critical). Edge arrows show dependency direction.</p>
</div>
<div id="graph-container">
<div id="mynetwork"></div>
</div>
<script>
// Global state for the graph
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let servicesData = [];
// Map criticality scores to colors (green=low, red=critical)
function getCriticalityColor(score) {
if (score >= 4) return { background: '#ff4757', border: '#ff6b81' };
if (score >= 3) return { background: '#ffa502', border: '#ffbe76' };
if (score >= 2) return { background: '#eccc68', border: '#f1e05a' };
return { background: '#2ed573', border: '#7bed9f' };
}
// Map impact percentage to CSS class for color coding
function getImpactClass(percentage) {
if (percentage >= 60) return 'impact-high';
if (percentage >= 30) return 'impact-medium';
return 'impact-low';
}
// Fetch services from the API and render the dependency graph
async function loadGraph() {
const response = await fetch('/api/services');
const data = await response.json();
servicesData = data.services;
const nodes = [];
const edges = [];
servicesData.forEach(service => {
const colors = getCriticalityColor(service.criticality_score);
nodes.push({
id: service.name,
label: service.name.replace('-service', '').replace('-', ' '),
color: colors,
font: { color: '#fff', size: 12 },
shape: 'dot',
size: 15 + service.criticality_score * 5,
title: `${service.name}\nOwner: ${service.owner}\nSLO: ${service.slo_tier}\nCriticality: ${service.criticality_score}`
});
service.depends_on.forEach(dep => {
edges.push({
from: dep,
to: service.name,
arrows: 'to',
color: { color: '#555', opacity: 0.6 },
smooth: { type: 'curvedCW', roundness: 0.2 }
});
});
});
nodesDataset = new vis.DataSet(nodes);
edgesDataset = new vis.DataSet(edges);
const container = document.getElementById('mynetwork');
const graphData = { nodes: nodesDataset, edges: edgesDataset };
const options = {
physics: { solver: 'forceAtlas2Based', forceAtlas2Based: { gravitationalConstant: -50, springLength: 150 } },
interaction: { hover: true, tooltipDelay: 100 },
edges: { width: 1.5 }
};
network = new vis.Network(container, graphData, options);
network.on('click', handleNodeClick);
}
// Handle node click: simulate failure and show blast radius
async function handleNodeClick(params) {
if (params.nodes.length === 0) return;
const serviceName = params.nodes[0];
const response = await fetch('/api/simulate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ service: serviceName })
});
const result = await response.json();
// Color the failed node dark red
nodesDataset.update({ id: serviceName, color: { background: '#ff4757', border: '#ff0000' } });
// Color all affected nodes orange-red
result.affected_services.forEach(name => {
nodesDataset.update({ id: name, color: { background: '#ff6348', border: '#ff4757' } });
});
// Update the blast radius panel
document.getElementById('blast-panel').style.display = 'block';
document.getElementById('failed-service').textContent = serviceName;
document.getElementById('affected-count').textContent = result.affected_count;
const impactEl = document.getElementById('user-impact');
impactEl.textContent = result.user_impact_percentage + '%';
impactEl.className = 'stat-value ' + getImpactClass(result.user_impact_percentage);
document.getElementById('teams-affected').textContent = result.affected_teams.length;
// Populate the affected services list
const list = document.getElementById('affected-list');
list.innerHTML = '';
result.affected_services.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
list.appendChild(li);
});
}
// Reset all services to healthy and reload the graph
async function resetSimulation() {
await fetch('/api/reset', { method: 'POST' });
loadGraph();
document.getElementById('blast-panel').style.display = 'none';
}
// Initialize the graph on page load
loadGraph();
</script>
</body>
</html>
What does this code do?
This file has three main sections working together.
- The <style> block creates a dark-themed layout with a 350px sidebar on the left and a full-height graph canvas on the right.
- The HTML body defines a sidebar with a hidden blast radius panel (revealed after a simulation), a service info panel, and a reset button.
- The <script> section fetches service data from your FastAPI backend, builds a vis-network graph with nodes sized and colored by criticality, and wires up click handlers for simulation.
Why vis-network.js over D3?
vis-network is purpose-built for network graphs. D3 is a general-purpose visualization library that requires significantly more code for the same result. The standalone UMD build loads from a CDN with zero build tools, keeping you focused on the SRE concepts instead of frontend tooling.
- Save static/index.html.
- Start the FastAPI server by running this command:
uvicorn app.main:app --reload
- Open your browser and navigate to http://localhost:8000.
You should see a dark-themed page with colored nodes connected by arrows. Each node represents a service from your registry, sized by criticality score.
Seeing a blank page or an error?
- Check that the static folder is in your project root (the same level as app/ and services.yaml).
- Open your browser's DevTools console (F12) and look for red error messages. A 404 on /api/services means the backend is not running.
- Make sure you started uvicorn from your project root directory, not from inside the app/ folder.
Why isn't my dependency graph showing up in the browser?
Test the click-to-kill interaction
The graph is not just a static picture. Every node is a trigger for a live cascading failure simulation powered by the API you built in the previous step.
- Click the payment node in the graph (it will be one of the larger, more orange/red nodes).
You should see the payment node turn dark red, followed by other dependent nodes turning orange-red as the failure propagates. The sidebar's Blast Radius panel should appear with simulation results.
How does click-to-kill work?
When you click a node, the handleNodeClick function fires. It sends a POST request to /api/simulate with the clicked service name.
The backend runs its breadth-first failure propagation and returns the blast radius. The frontend then updates node colors in real-time using nodesDataset.update() so you can visually see the cascade spread.
Clicking a node does nothing?
- Make sure you are clicking directly on a node (the colored circle), not on the empty space between nodes.
- Open DevTools (F12) and check the Console tab for errors. A POST /api/simulate 404 means the backend endpoint is not running.
Why doesn't clicking a node trigger the failure simulation?
Explore the blast radius summary panel
The sidebar panel shows the real-world information an SRE team needs during an incident. It tells you exactly who is affected and how many users are impacted.
- Look at the Blast Radius panel in the sidebar. Confirm you see the failed service name, affected service count, user impact percentage, and number of teams affected.
- Scroll down in the panel to see the full list of affected services.
- Click the green Reset All Services button at the bottom of the sidebar.
The graph should reset to its original colors, and the blast radius panel should disappear. All nodes return to their criticality-based coloring.
What do the node colors mean?
- Green nodes (score below 2) have low blast radius. Losing them affects few or no other services.
- Yellow nodes (score 2-3) have moderate impact and would degrade some functionality.
- Orange nodes (score 3-4) are high-impact services with many downstream dependents.
- Red nodes (score 4+) are critical. Losing one of these cascades across a large portion of your system.
- Try clicking the api-gateway node (the largest red node). Notice that nearly every service goes red because almost everything depends on the gateway.
Why is api-gateway the most critical?
The api-gateway has no upstream dependencies itself, but 6 services directly depend on it. Those 6 services have their own dependents, creating a massive transitive blast radius. This is exactly the kind of insight SRE teams need to prioritize reliability investments.
✔️ Awesome, I've got everything!
Great. Double check you have saved static/index.html and that the graph is rendering with colored nodes in your browser.
ⓧ I'd like to double check the full code
Here is the complete static/index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Dependency Mapper</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; display: flex; height: 100vh; }
#sidebar { width: 350px; padding: 20px; background: #16213e; overflow-y: auto; border-right: 1px solid #0f3460; }
#sidebar h1 { font-size: 1.3rem; margin-bottom: 10px; color: #00d4ff; }
#sidebar p { font-size: 0.85rem; color: #aaa; margin-bottom: 20px; }
#graph-container { flex: 1; }
#mynetwork { width: 100%; height: 100%; }
.panel { background: #0f3460; border-radius: 8px; padding: 15px; margin-bottom: 15px; }
.panel h3 { font-size: 0.95rem; margin-bottom: 8px; color: #00d4ff; }
.stat { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid #1a1a2e; font-size: 0.85rem; }
.stat-value { font-weight: bold; color: #00d4ff; }
.impact-high { color: #ff4757; }
.impact-medium { color: #ffa502; }
.impact-low { color: #2ed573; }
.affected-list { list-style: none; padding: 0; }
.affected-list li { padding: 4px 8px; margin: 3px 0; background: #1a1a2e; border-radius: 4px; font-size: 0.8rem; }
.btn { width: 100%; padding: 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem; margin-top: 10px; }
.btn-reset { background: #2ed573; color: #1a1a2e; font-weight: bold; }
.btn-reset:hover { background: #26bf65; }
#instructions { font-size: 0.8rem; color: #888; margin-top: 15px; }
</style>
</head>
<body>
<div id="sidebar">
<h1>Service Dependency Mapper</h1>
<p>Click any service node to simulate a failure and see the blast radius.</p>
<div class="panel" id="blast-panel" style="display:none;">
<h3>Blast Radius</h3>
<div class="stat"><span>Failed Service</span><span class="stat-value" id="failed-service">-</span></div>
<div class="stat"><span>Affected Services</span><span class="stat-value" id="affected-count">-</span></div>
<div class="stat"><span>User Impact</span><span class="stat-value" id="user-impact">-</span></div>
<div class="stat"><span>Teams Affected</span><span class="stat-value" id="teams-affected">-</span></div>
<h3 style="margin-top: 12px;">Affected Services</h3>
<ul class="affected-list" id="affected-list"></ul>
</div>
<div class="panel" id="service-panel">
<h3>Service Info</h3>
<p id="service-info-text" style="font-size: 0.85rem; color: #aaa;">Select a service to see details</p>
</div>
<button class="btn btn-reset" onclick="resetSimulation()">Reset All Services</button>
<p id="instructions">Nodes are colored by criticality: green (low) to red (critical). Edge arrows show dependency direction.</p>
</div>
<div id="graph-container">
<div id="mynetwork"></div>
</div>
<script>
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let servicesData = [];
function getCriticalityColor(score) {
if (score >= 4) return { background: '#ff4757', border: '#ff6b81' };
if (score >= 3) return { background: '#ffa502', border: '#ffbe76' };
if (score >= 2) return { background: '#eccc68', border: '#f1e05a' };
return { background: '#2ed573', border: '#7bed9f' };
}
function getImpactClass(percentage) {
if (percentage >= 60) return 'impact-high';
if (percentage >= 30) return 'impact-medium';
return 'impact-low';
}
async function loadGraph() {
const response = await fetch('/api/services');
const data = await response.json();
servicesData = data.services;
const nodes = [];
const edges = [];
servicesData.forEach(service => {
const colors = getCriticalityColor(service.criticality_score);
nodes.push({
id: service.name,
label: service.name.replace('-service', '').replace('-', ' '),
color: colors,
font: { color: '#fff', size: 12 },
shape: 'dot',
size: 15 + service.criticality_score * 5,
title: `${service.name}\nOwner: ${service.owner}\nSLO: ${service.slo_tier}\nCriticality: ${service.criticality_score}`
});
service.depends_on.forEach(dep => {
edges.push({
from: dep,
to: service.name,
arrows: 'to',
color: { color: '#555', opacity: 0.6 },
smooth: { type: 'curvedCW', roundness: 0.2 }
});
});
});
nodesDataset = new vis.DataSet(nodes);
edgesDataset = new vis.DataSet(edges);
const container = document.getElementById('mynetwork');
const graphData = { nodes: nodesDataset, edges: edgesDataset };
const options = {
physics: { solver: 'forceAtlas2Based', forceAtlas2Based: { gravitationalConstant: -50, springLength: 150 } },
interaction: { hover: true, tooltipDelay: 100 },
edges: { width: 1.5 }
};
network = new vis.Network(container, graphData, options);
network.on('click', handleNodeClick);
}
async function handleNodeClick(params) {
if (params.nodes.length === 0) return;
const serviceName = params.nodes[0];
const response = await fetch('/api/simulate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ service: serviceName })
});
const result = await response.json();
nodesDataset.update({ id: serviceName, color: { background: '#ff4757', border: '#ff0000' } });
result.affected_services.forEach(name => {
nodesDataset.update({ id: name, color: { background: '#ff6348', border: '#ff4757' } });
});
document.getElementById('blast-panel').style.display = 'block';
document.getElementById('failed-service').textContent = serviceName;
document.getElementById('affected-count').textContent = result.affected_count;
const impactEl = document.getElementById('user-impact');
impactEl.textContent = result.user_impact_percentage + '%';
impactEl.className = 'stat-value ' + getImpactClass(result.user_impact_percentage);
document.getElementById('teams-affected').textContent = result.affected_teams.length;
const list = document.getElementById('affected-list');
list.innerHTML = '';
result.affected_services.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
list.appendChild(li);
});
}
async function resetSimulation() {
await fetch('/api/reset', { method: 'POST' });
loadGraph();
document.getElementById('blast-panel').style.display = 'none';
}
loadGraph();
</script>
</body>
</html>
Your dependency mapper now has a fully interactive frontend. You can visualize your entire service architecture at a glance and simulate failures with a single click. Next up, you will instrument the application with Prometheus metrics so you can monitor the mapper itself.
Deploy the Observability Stack with Docker Compose
Your mapper runs locally and produces Prometheus metrics, but nobody is collecting them. Without a scraper pulling those metrics and a dashboard displaying them, the data disappears every time you restart the app.
In this step, you will containerize the mapper with Docker and wire it into a full observability stack. Docker Compose will orchestrate three containers: the mapper, Prometheus for scraping and alerting, and Grafana for dashboarding.
In this step, get ready to:
- Create a Dockerfile and Docker Compose configuration to run the mapper, Prometheus, and Grafana.
- Configure Prometheus scraping and SLO-based alert rules.
- Provision Grafana with a Prometheus datasource and start the full stack.
Create the Dockerfile
A Dockerfile tells Docker how to package your Python app into a portable container image. This image will contain your code, dependencies, and the command to start the server.
- Create a new file called Dockerfile in the root of your dependency-mapper project folder (next to requirements.txt).
- Paste the following into Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
What does this Dockerfile do?
- FROM python:3.12-slim starts from a lightweight Python image.
- COPY requirements.txt and RUN pip install installs dependencies before copying the rest of the code. This layer caching means Docker skips reinstalling packages when only your code changes.
- CMD launches uvicorn on port 8000, binding to all interfaces so Docker can forward traffic in.
- Save Dockerfile.
Create the Docker Compose configuration
Docker Compose lets you define multiple containers that start together and communicate over a shared network. Your stack needs three services: the mapper app, Prometheus, and Grafana.
- Create a new file called docker-compose.yml in the root of your project folder.
- Paste the following into docker-compose.yml:
services:
mapper:
build: .
container_name: dependency-mapper
ports:
- "8000:8000"
volumes:
- ./services.yaml:/app/services.yaml:ro
- ./static:/app/static:ro
networks:
- monitoring
restart: unless-stopped
prometheus:
image: prom/prometheus:v3.10.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/rules.yml:/etc/prometheus/rules.yml:ro
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=7d'
- '--web.enable-lifecycle'
networks:
- monitoring
restart: unless-stopped
grafana:
image: grafana/grafana:12.4.1
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: sreproject
GF_USERS_ALLOW_SIGN_UP: "false"
networks:
- monitoring
depends_on:
- prometheus
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:
networks:
monitoring:
driver: bridge
What does this configuration do?
- mapper builds the Dockerfile and runs your FastAPI app on port 8000. The services.yaml and static/ are mounted as read-only volumes so you can edit them without rebuilding.
- prometheus pulls the official Prometheus image (v3.10.0) and mounts your config files. It retains 7 days of metric data.
- grafana pulls Grafana 12.4.1 and auto-provisions its datasource from a YAML file you will create next.
- All three services share a monitoring bridge network. Docker DNS lets them reach each other by container name (e.g. mapper:8000), not localhost.
- Save docker-compose.yml.
Not sure if the file is saved correctly?
YAML is sensitive to indentation. Make sure you use spaces (not tabs) and that each nested level is indented by exactly 2 spaces. VS Code shows a warning icon in the file tab if there are syntax errors.
Help me fix YAML indentation in my docker-compose.yml
✔️ Awesome, I've got everything!
Great. Make sure both Dockerfile and docker-compose.yml are saved at the root of your project folder.
ⓧ I'd like to double check the full code
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
services:
mapper:
build: .
container_name: dependency-mapper
ports:
- "8000:8000"
volumes:
- ./services.yaml:/app/services.yaml:ro
- ./static:/app/static:ro
networks:
- monitoring
restart: unless-stopped
prometheus:
image: prom/prometheus:v3.10.0
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/rules.yml:/etc/prometheus/rules.yml:ro
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=7d'
- '--web.enable-lifecycle'
networks:
- monitoring
restart: unless-stopped
grafana:
image: grafana/grafana:12.4.1
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: sreproject
GF_USERS_ALLOW_SIGN_UP: "false"
networks:
- monitoring
depends_on:
- prometheus
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:
networks:
monitoring:
driver: bridge
Configure Prometheus scraping and alert rules
Prometheus needs to know where to find your app's metrics endpoint. It also needs alert rules that define when something is wrong, like your SLO error budget burning too fast.
- Create the monitoring directory by running this command in your terminal:
mkdir -p monitoring
- Create a new file called monitoring/prometheus.yml and paste the following:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "rules.yml"
scrape_configs:
- job_name: 'dependency-mapper'
static_configs:
- targets: ['mapper:8000']
metrics_path: '/metrics/'
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
What does this config do?
- scrape_interval: 15s means Prometheus pulls metrics from your app every 15 seconds.
- The target mapper:8000 uses Docker's internal DNS. Inside the monitoring network, containers reach each other by name.
- The metrics_path is /metrics/ with a trailing slash. FastAPI's sub-app mounting behavior causes a redirect without it, so the trailing slash is required.
- Save monitoring/prometheus.yml.
- Now create monitoring/rules.yml and paste the following alert rules:
groups:
- name: mapper_slo
rules:
- record: mapper:request_error_rate:5m
expr: |
sum(rate(mapper_request_duration_seconds_count{endpoint="/api/simulate"}[5m])) > 0
and
(
sum(rate(mapper_request_duration_seconds_bucket{endpoint="/api/simulate",le="0.5"}[5m]))
/
sum(rate(mapper_request_duration_seconds_count{endpoint="/api/simulate"}[5m]))
)
- alert: SLOBudgetBurning
expr: |
(
sum(rate(mapper_request_duration_seconds_bucket{endpoint="/api/simulate",le="0.5"}[5m]))
/
sum(rate(mapper_request_duration_seconds_count{endpoint="/api/simulate"}[5m]))
) < 0.995
for: 2m
labels:
severity: warning
annotations:
summary: "Mapper SLO budget is burning"
description: "Less than 99.5% of simulation requests are completing within 500ms over the last 5 minutes."
- alert: HighErrorRate
expr: |
sum(rate(mapper_requests_total[5m])) > 0
and
(
sum(rate(mapper_requests_total{endpoint="/api/simulate"}[5m]))
/
sum(rate(mapper_requests_total[5m]))
) > 0.5
for: 1m
labels:
severity: critical
annotations:
summary: "High proportion of simulation requests"
description: "More than 50% of all requests are simulations, which may indicate automated abuse."
What do these alert rules do?
- The recording rule (mapper:request_error_rate:5m) pre-computes the percentage of simulation requests that finish within 500ms. This makes SLO dashboards faster to load.
- SLOBudgetBurning fires when fewer than 99.5% of requests meet the 500ms latency target over a 2-minute window. This is your error budget alarm.
- HighErrorRate fires when more than 50% of all requests are simulations for 1 minute straight. This could indicate automated abuse or a misconfigured client.
What is an error budget?
An error budget is the amount of unreliability your service is "allowed" before it breaches its SLO. If your SLO says 99.5% of requests must be under 500ms, your error budget is the remaining 0.5%. Once that budget is exhausted, the alert fires.
- Save monitoring/rules.yml.
- Confirm both files exist by running:
ls monitoring/
You should see prometheus.yml and rules.yml in the output.
Don't see both files?
Make sure you created the files inside the monitoring/ directory, not in the project root. The path should be monitoring/prometheus.yml and monitoring/rules.yml.
Help me find my monitoring config files
✔️ Awesome, I've got everything!
Great. Double check both files are saved inside the monitoring/ directory.
ⓧ I'd like to double check the full code
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "rules.yml"
scrape_configs:
- job_name: 'dependency-mapper'
static_configs:
- targets: ['mapper:8000']
metrics_path: '/metrics/'
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
groups:
- name: mapper_slo
rules:
- record: mapper:request_error_rate:5m
expr: |
sum(rate(mapper_request_duration_seconds_count{endpoint="/api/simulate"}[5m])) > 0
and
(
sum(rate(mapper_request_duration_seconds_bucket{endpoint="/api/simulate",le="0.5"}[5m]))
/
sum(rate(mapper_request_duration_seconds_count{endpoint="/api/simulate"}[5m]))
)
- alert: SLOBudgetBurning
expr: |
(
sum(rate(mapper_request_duration_seconds_bucket{endpoint="/api/simulate",le="0.5"}[5m]))
/
sum(rate(mapper_request_duration_seconds_count{endpoint="/api/simulate"}[5m]))
) < 0.995
for: 2m
labels:
severity: warning
annotations:
summary: "Mapper SLO budget is burning"
description: "Less than 99.5% of simulation requests are completing within 500ms over the last 5 minutes."
- alert: HighErrorRate
expr: |
sum(rate(mapper_requests_total[5m])) > 0
and
(
sum(rate(mapper_requests_total{endpoint="/api/simulate"}[5m]))
/
sum(rate(mapper_requests_total[5m]))
) > 0.5
for: 1m
labels:
severity: critical
annotations:
summary: "High proportion of simulation requests"
description: "More than 50% of all requests are simulations, which may indicate automated abuse."
Provision Grafana and start the full stack
Grafana can auto-configure datasources from YAML files at startup. This means you do not need to manually click through the Grafana UI to connect Prometheus.
- Create the Grafana provisioning directory structure by running:
mkdir -p monitoring/grafana/provisioning/datasources
- Create a new file at monitoring/grafana/provisioning/datasources/datasource.yml and paste the following:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
What does this datasource config do?
This tells Grafana to connect to Prometheus at http://prometheus:9090 using Docker's internal DNS. The access: proxy setting means Grafana makes requests to Prometheus on the backend, not from your browser.
- Save datasource.yml.
- If you still have the local uvicorn server running from a previous step, stop it by pressing Ctrl+C in that terminal.
- Start the full stack by running:
docker compose up --build -d
What does this command do?
- --build builds the mapper image from your Dockerfile before starting.
- -d runs all containers in the background (detached mode) so you get your terminal back.
Docker will pull the Prometheus and Grafana images, build your mapper image, and start all three containers. This may take 1-2 minutes the first time.
- Verify all three containers are running by executing:
docker compose ps
You should see three containers listed with a status of Up: dependency-mapper, prometheus, and grafana.
✔️ All three containers show Up
Your stack is running. Now verify each service is reachable.
- Open http://localhost:8000 in your browser. You should see the dependency graph.
- Open http://localhost:9090 to see the Prometheus UI.
- Open http://localhost:3000 to see the Grafana login page. Log in with username admin and password sreproject.
ⓧ One or more containers show Exited
Check the logs of the failing container to see the error. Replace the container name with the one showing Exited:
docker compose logs mapper
- If mapper failed, check that requirements.txt exists in the project root and that your Dockerfile is saved correctly.
- If prometheus failed, check for YAML syntax errors in monitoring/prometheus.yml or monitoring/rules.yml.
- If grafana failed, verify the provisioning directory structure: monitoring/grafana/provisioning/datasources/datasource.yml.
Help me debug my Docker Compose stack
- In the Prometheus UI at http://localhost:9090, click Status in the top navigation bar.
- Click Targets.
You should see two targets listed: dependency-mapper and prometheus. Both should show a green UP state.
Target showing DOWN?
If the dependency-mapper target shows a red DOWN state, click on the error message. The most common cause is a wrong metrics_path in prometheus.yml. Make sure it is /metrics/ with a trailing slash.
Help me fix my Prometheus target
Your observability stack is live. Prometheus is scraping your mapper's metrics every 15 seconds, alert rules are evaluating your SLO budget, and Grafana is ready to visualize it all. Next up, you will run a cascading failure simulation and watch the metrics update in real-time.
Simulate an Outage and Verify the Stack
Your full observability stack is running. The backend, Prometheus, and Grafana are all up and networked together.
Now it is time to answer the question this entire tool was built for: "If payment-service dies, what breaks?" You will run a cascading failure simulation and then verify that the observability stack captures every metric from that event.
In this step, get ready to:
- Verify the dependency graph loads in your browser with 10 service nodes.
- Simulate a payment-service outage and observe the blast radius cascade.
- Confirm Prometheus and Grafana are capturing your simulation metrics and SLO tracking.
Verify the stack and explore the dependency graph
Before simulating any failures, you need to confirm that all three containers are healthy and the frontend loads correctly.
- Verify all three containers are running by executing this command in your terminal:
docker compose ps
You should see three containers listed: dependency-mapper, prometheus, and grafana, all with a status of "running".
- Open your browser and navigate to http://localhost:8000.
You should see the Service Dependency Mapper interface. On the left is a sidebar with a "Blast Radius" panel and a "Reset All Services" button. On the right is an interactive graph showing 10 service nodes connected by dependency arrows.
- Hover over different nodes in the graph to see tooltip details (service name, owner, SLO tier, and criticality score).
What do the node colors mean?
Each node is colored by its criticality score. Green nodes (like analytics-service) have low blast radius potential. Yellow and orange nodes have moderate impact. Red nodes (like api-gateway) would take down most of the system if they failed.
Node size also scales with criticality. The larger the node, the more services depend on it transitively.
- Verify the health endpoint is responding by opening a new tab and navigating to http://localhost:8000/api/health.
You should see a JSON response: {"status":"healthy"}.
Don't see the graph or getting an error?
If you see a blank page, open the browser DevTools console (press F12) and look for network errors. The vis-network.js library loads from a CDN, so you need an internet connection.
If you see "connection refused", confirm the mapper container is running with docker compose ps. If it shows "exited", check logs with docker compose logs mapper.
Help me troubleshoot why my dependency mapper isn't loading in the browser
Simulate a payment-service outage
The payment-service is one of the most critical services in the e-commerce platform. It processes all transactions, and multiple other services depend on it. Killing it will trigger a realistic cascade.
- In your browser at http://localhost:8000, find the node labeled payment in the graph (it is one of the larger, red-tinted nodes).
- Click the payment node.
The moment you click, the simulation fires. You should see several things happen at once: the payment node turns bright red, and other nodes that depend on it also turn red as the failure cascades through the graph.
- Look at the Blast Radius panel that appeared in the sidebar.
The panel shows four key metrics from your simulation. The Failed Service should say payment-service. The Affected Services count shows how many services cascaded into failure.
What services should be affected?
When payment-service goes down, the cascading failure simulator uses breadth-first traversal on the reverse dependency graph. It finds that order-service depends on payment-service directly. Then shipping-service depends on order-service, and notification-service depends on both order-service and user-service.
The user impact percentage reflects what fraction of total user-facing functionality is now degraded. A payment-service outage has a significant blast radius because it sits upstream of the entire order flow.
- Scroll down in the Blast Radius panel to see the list of affected services and the number of teams impacted.
- Click the Reset All Services button to restore all nodes to healthy.
The graph reloads and all nodes return to their original criticality-based colors. The reset calls the /api/reset endpoint, which sets every service health gauge back to 1.
Verify metrics in Prometheus and Grafana
The simulation you just ran generated real Prometheus metrics. Every API call incremented counters, recorded latency histograms, and toggled service health gauges. Now you will confirm that Prometheus scraped these metrics and that Grafana can visualize them.
- Open a new browser tab and navigate to http://localhost:9090.
This is the Prometheus web UI. You will use it to confirm that your custom metrics are being scraped from the mapper.
- Click on Status in the top navigation bar, then select Targets.
You should see two targets listed: dependency-mapper and prometheus. Both should show a state of "UP" with a green indicator.
- Click on the Prometheus logo or Graph in the top navigation to return to the query page.
- Type mapper_requests_total in the expression field and click Execute.
You should see results showing request counts with labels for method and endpoint. The /api/services and /api/simulate endpoints should both have non-zero values from your earlier interactions.
- Try querying mapper_simulations_total to see the simulation counter.
- Query mapper_service_health to see the health gauge for all 10 services (all should be 1 if you reset earlier).
What about the SLO recording rule?
Navigate to Status then Rules in the Prometheus UI. You should see the mapper_slo group with the mapper:request_error_rate:5m recording rule and the SLOBudgetBurning alert rule loaded.
The recording rule only produces a value once there are enough simulation requests within a 5-minute window. Run a few more simulations in the graph and check back after a minute to see it populate.
Now verify that Grafana is connected to Prometheus and can visualize your metrics.
- Open a new browser tab and navigate to http://localhost:3000.
- Log in with username admin and password sreproject.
You should see the Grafana home page. The Prometheus datasource was automatically provisioned by the configuration file you created earlier.
- Click the hamburger menu icon in the top left corner.
- Select Connections, then Data sources.
- Confirm that Prometheus is listed as a datasource with the URL http://prometheus:9090.
Now create a quick query to verify RED metrics (Rate, Errors, Duration) are flowing into Grafana.
- Click the hamburger menu icon again.
- Select Explore.
- Make sure Prometheus is selected as the datasource in the top dropdown.
- Enter this query in the metrics field:
rate(mapper_requests_total[5m])
- Click Run query.
You should see a graph showing the request rate over time. If you ran simulations earlier, there will be visible spikes in the data. This confirms Grafana can query your mapper's metrics through Prometheus.
- Try querying histogram_quantile(0.95, rate(mapper_request_duration_seconds_bucket[5m])) to see the p95 latency (Duration from the RED method).
What are RED metrics?
RED stands for Rate, Errors, and Duration. It is the standard method for monitoring request-driven services. Rate tells you how many requests per second your API handles. Errors shows what percentage are failing. Duration reveals how long requests take.
Your mapper exposes all three via the mapper_requests_total counter (rate), HTTP status codes (errors), and mapper_request_duration_seconds histogram (duration).
No data showing in Grafana?
If the Explore query returns no data, first go back to http://localhost:9090/targets and confirm the dependency-mapper target shows "UP". If it shows "DOWN", the Prometheus container cannot reach the mapper container on the Docker network.
Also confirm you have run at least one simulation by clicking a node in the graph. Prometheus only shows data for metrics that have been incremented at least once.
Help me troubleshoot why Grafana is not showing data from my Prometheus datasource
- Go back to the dependency graph at http://localhost:8000 and click a few more service nodes to generate additional simulation data.
- Return to Grafana Explore and re-run your queries to see the new data points appear.
Finally, commit your completed project and push it to GitHub.
- Commit and push all your work by running these commands:
git add .
git commit -m "Complete service dependency mapper with observability stack"
git push origin main
Your service dependency mapper is fully operational. You have a working blast radius prediction tool backed by a production-grade observability stack. Next up, you can extend this further with a GameDay chaos mode in the Secret Mission.
Secret mission
GameDay Chaos Mode
Real SRE teams run GameDays to test their systems under controlled chaos. Can you build a chaos engineering feature that randomly kills services on a timer and tracks how long each one takes to recover?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Docker containers, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers: dependency-mapper, prometheus, grafana.
- Docker volumes: prometheus-data and grafana-data.
- Python virtual environment (venv/ folder).
- Project directory on your Desktop.
- GitHub repository.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with the dependency mapper and observability stack.
- Your Docker containers will keep running until you stop them or restart your machine.
- Access the dependency graph at http://localhost:8000 and Grafana at http://localhost:3000 anytime.
Pause - I'll come back to this later
Shut down running containers to free up memory and CPU while keeping all your files and configuration intact.
- Open your terminal and navigate to your project folder.
- Stop all containers by running:
docker compose down
- This stops and removes the containers but preserves your Docker volumes (Prometheus and Grafana data).
- When you are ready to resume, run docker compose up from the project folder to bring everything back up.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes containers, volumes, images, and project files permanently.
- Open your terminal and navigate to your project folder.
- Stop containers and remove volumes by running:
docker compose down -v --rmi all
- This removes containers, networks, volumes, and images built for this project.
- Delete the project folder from your Desktop.
- Optionally, delete the GitHub repository from your GitHub account under Settings > General > Danger Zone > Delete this repository.
Nice Work!
Nice Work!
You just built a service dependency mapper with blast radius prediction, backed by a full observability stack.
You've learned how to:
- Build a Python FastAPI application with graph traversal algorithms that compute criticality scores and simulate cascading failures across a microservices architecture.
- Create an interactive vis-network.js visualization that communicates complex system dependencies at a glance, with click-to-kill failure simulation.
- Deploy a Prometheus + Grafana observability stack with custom metrics, SLO-based alerting, and Docker Compose orchestration.
- Added GameDay chaos mode with MTTR tracking for realistic chaos engineering practice.
Ready to quiz yourself?