MCP Gateway Observability Architecture for AWS ECS (Terraform)
This document describes the observability architecture for the MCP Gateway Registry when deployed on AWS ECS using Terraform.
Executive Summary
The Terraform ECS deployment uses the existing metrics-service to aggregate application metrics from the registry and auth-server, with AWS-native services (Amazon Managed Prometheus, Grafana OSS on ECS) providing durable storage and visualization. The ADOT collector runs as a sidecar in the metrics-service task, scraping its Prometheus endpoint and remote-writing to AMP. All observability resources are gated by var.enable_observability (default: true) and can be fully disabled with a single variable.
Architecture Overview
The pipeline reuses the same metrics-service that runs in local docker-compose development. The registry and auth-server already emit metrics to METRICS_SERVICE_URL via HTTP POST -- no application code changes are required. In the AWS deployment, AMP replaces the local Prometheus container as the durable time-series store, and Grafana OSS on ECS replaces the local Grafana container. The same Grafana dashboards work in both environments because they query identical Prometheus metric names.
ADOT Sidecar Pattern
The ADOT collector runs as a sidecar container within the metrics-service ECS task definition, scraping localhost:9465. This is necessary because the Terraform deployment uses the terraform-aws-modules/ecs module, which creates HTTP-type Cloud Map services. HTTP-type services register in Cloud Map but do not create Route53 A records -- DNS resolution is handled by the ECS Service Connect Envoy sidecar proxy. A standalone ADOT service cannot resolve Service Connect hostnames via system DNS, but co-locating as a sidecar eliminates the DNS dependency entirely.
See issue #496 for the broader discussion on Service Connect DNS behavior.
Ephemeral SQLite
The metrics-service uses SQLite for local buffering but ECS Fargate task storage is ephemeral. AMP serves as the durable store (150-day default retention), replacing SQLite's historical analysis role. SQLite data loss on task restart has no impact on the metrics pipeline.
Architecture
+---------------------------------------------------------------------+
| ECS Services |
| |
| +-----------------+ +-----------------+ +---------------------+ |
| | Registry | | Auth Server | | Other Services | |
| | | | | | (MCP Servers) | |
| | METRICS_SERVICE | | METRICS_SERVICE | | No custom metrics | |
| | _URL=http:// | | _URL=http:// | | (CloudWatch only) | |
| | metrics:8890 | | metrics:8890 | | | |
| | METRICS_API_KEY | | METRICS_API_KEY | | | |
| +--------+--------+ +--------+--------+ +---------------------+ |
| | | |
| +--------------------+ |
| | |
+--------------------------------+-------------------------------------+
|
| HTTP POST /metrics
| Header: X-API-Key: <METRICS_API_KEY>
v
+---------------------------------------------------+
| metrics-service ECS Task (512 CPU, 1024 MB) |
| |
| +---------------------------------------------+ |
| | metrics-service container | |
| | | |
| | +---------------------------------------+ | |
| | | FastAPI Application | | |
| | | - Receives metrics via HTTP API | | |
| | | - API key auth (METRICS_API_KEY_*) | | |
| | | - Rate limiting (1000 req/min) | | |
| | | - Request validation | | |
| | | - In-memory buffering (5s flush) | | |
| | +---------------------------------------+ | |
| | | |
| | +---------------------------------------+ | |
| | | OpenTelemetry Instrumentation | | |
| | | - Counters: auth, tool, discovery | | |
| | | - Histograms: latency, duration | | |
| | | - Custom bucket boundaries (5ms-300s) | | |
| | | - Prometheus exporter :9465 | | |
| | +---------------------------------------+ | |
| | | |
| | Ports: 8890 (API), 9465 (Prometheus) | |
| +---------------------------------------------+ |
| |
| +---------------------------------------------+ |
| | adot-collector sidecar container | |
| | | |
| | +---------------------------------------+ | |
| | | Prometheus Receiver | | |
| | | - Scrapes localhost:9465 | | |
| | | - 15s scrape interval | | |
| | +---------------------------------------+ | |
| | | |
| | +---------------------------------------+ | |
| | | Prometheus Remote Write Exporter | | |
| | | - SigV4 authentication | | |
| | | - Writes to AMP workspace | | |
| | +---------------------------------------+ | |
| | | |
| | Health check: :13133 | |
| | essential: false (metrics-service can run | |
| | without ADOT; metrics just won't reach AMP)| |
| +---------------------------------------------+ |
| |
+------------------------+--------------------------+
|
| Remote Write (SigV4)
| https://aps-workspaces.region.amazonaws.com
v
+---------------------------------------------------+
| Amazon Managed Prometheus (AMP) |
| |
| - Fully managed Prometheus-compatible |
| - Automatic scaling |
| - 150-day default retention |
| - PromQL query support |
| - SigV4 authentication |
| - No infrastructure to manage |
| |
| Alert Rules: |
| - MCPHighErrorRate (>10% for 5 min) |
| - MCPRegistryDown (no requests for 5 min) |
| - MCPHighLatency (P95 > 5s for 5 min) |
+------------------------+--------------------------+
|
| PromQL Queries (SigV4)
v
+---------------------------------------------------+
| Grafana OSS ECS Task (512 CPU, 1024 MB) |
| |
| +---------------------------------------------+ |
| | Pre-configured Datasource | |
| | - Amazon Managed Prometheus (AMP) | |
| | - SigV4 auth via IAM task role | |
| +---------------------------------------------+ |
| |
| +---------------------------------------------+ |
| | Pre-loaded Dashboard: MCP Analytics | |
| | - Real-time Protocol Activity | |
| | - Authentication Flow Analysis | |
| | - Active MCP Servers | |
| | - Tool Executions per Hour | |
| | - MCP Latency P95 (by Server & Method) | |
| | - Server Performance Dashboard | |
| | - Tool Usage Rankings | |
| | - Error Rate Analysis | |
| | - Client Applications Distribution | |
| | - 19 panels total | |
| +---------------------------------------------+ |
| |
| Access: https://<cloudfront>/grafana/ |
| Auth: admin / grafana_admin_password (from tfvars)|
+---------------------------------------------------+
Component Details
Services Emitting Metrics
The following services emit custom metrics to the metrics-service:
| Service | Metrics Emitted | Configuration |
|---|---|---|
| Registry | Tool discovery, registry operations, health checks | METRICS_SERVICE_URL + METRICS_API_KEY env vars |
| Auth-server | Authentication requests (via /validate subrequest), session operations |
METRICS_SERVICE_URL + METRICS_API_KEY env vars |
| Nginx (Lua) | MCP tool execution counters and duration histograms | METRICS_API_KEY_NGINX env var. See PR #488. |
Note: MCP servers (CurrentTime, MCPGW, RealServerFakeTools, etc.) do not emit custom metrics directly. However, nginx emits tool execution metrics on their behalf via log_by_lua -- capturing method, tool name, duration, and success/failure for all MCP protocol traffic flowing through nginx location blocks.
MCP data-plane metrics: MCP protocol traffic (initialize, tools/list, tools/call) is handled by nginx location blocks and proxied directly to backend servers, bypassing FastAPI entirely. The middleware in registry/metrics/middleware.py never observes these requests. The auth-server sees every request via auth_request /validate, but the auth check fires before proxy_pass -- so it captures auth latency but cannot observe tool execution duration, success/failure, or which tool was called. The nginx Lua metrics pipeline (emit_metrics.lua + flush_metrics.lua, PR #488) fills this gap.
The metrics emission flow:
- Registry/Auth-server (control plane): Instantiate
MetricsClientfromregistry/metrics/client.py. The client readsMETRICS_SERVICE_URLandMETRICS_API_KEYfrom environment variables. Metrics are sent via HTTP POST to{METRICS_SERVICE_URL}/metricswithX-API-Keyheader. - Nginx (data plane, PR #488):
emit_metrics.luaruns inlog_by_luaphase after each MCP request, writing metrics tolua_shared_dict metrics_buffer 10m(no network I/O). A background timer inflush_metrics.lua(init_worker_by_lua) batch-POSTs buffered metrics to metrics-service every 5-10 seconds, authenticating withMETRICS_API_KEY_NGINX.
API Key Authentication Configuration
The metrics-service uses a dual naming convention for API keys:
Client Side (registry, auth-server):
- Environment variable:
METRICS_API_KEY - Used to authenticate when sending metrics to metrics-service
- In Terraform: sourced from
aws_secretsmanager_secret.metrics_api_key, auto-generated viarandom_password
Server Side (metrics-service):
- Environment variable pattern:
METRICS_API_KEY_<SERVICE> - The
setup_preshared_api_keys()function inmetrics-service/app/main.pydiscovers all environment variables matchingMETRICS_API_KEY_*on startup - Each key is automatically registered with the service name derived from the suffix (e.g.,
METRICS_API_KEY_REGISTRYregisters key for serviceregistry)
Terraform Implementation:
random_password.metrics_api_keygenerates a 32-character keyaws_secretsmanager_secret.metrics_api_keystores it in Secrets Manager- Registry and auth-server task definitions reference the secret as
METRICS_API_KEY - metrics-service receives the same secret as both
METRICS_API_KEY_REGISTRYandMETRICS_API_KEY_AUTH - All secret resources are gated by
var.enable_observability
To rotate the API key: update the secret in Secrets Manager and force redeploy the affected ECS services.
metrics-service
The metrics-service is deployed as an ECS Fargate task:
| Configuration | Value | Notes |
|---|---|---|
| Image | var.metrics_service_image_uri |
Built via CodeBuild or provided |
| CPU | 512 | 0.5 vCPU (shared with ADOT sidecar) |
| Memory | 1024 | 1 GB (shared with ADOT sidecar) |
| Port 8890 | HTTP API | Receives metrics from services |
| Port 9465 | Prometheus | Scraped by ADOT sidecar on localhost |
| Health Check | GET /health |
30s interval, 30s start period |
| Service Connect | metrics-service:8890 |
Discoverable by registry and auth-server |
Environment variables:
METRICS_SERVICE_HOST=0.0.0.0
PORT=8890
OTEL_SERVICE_NAME=mcp-metrics-service
OTEL_PROMETHEUS_ENABLED=true
OTEL_PROMETHEUS_PORT=9465
METRICS_RATE_LIMIT=1000
HISTOGRAM_BUCKET_BOUNDARIES=0.005,0.01,0.025,0.05,0.075,0.1,0.25,0.5,0.75,1.0,2.5,5.0,7.5,10.0,30.0,60.0,120.0,300.0
SQLITE_DB_PATH=/tmp/metrics.db
METRICS_API_KEY_REGISTRY=<from Secrets Manager>
METRICS_API_KEY_AUTH=<from Secrets Manager>
ADOT Collector (Sidecar)
AWS Distro for OpenTelemetry collector runs as a sidecar in the metrics-service task:
| Configuration | Value | Notes |
|---|---|---|
| Image | public.ecr.aws/aws-observability/aws-otel-collector:latest |
AWS-managed |
| CPU | 256 | Allocated within the 512 task CPU |
| Memory | 512 | Allocated within the 1024 task memory |
| essential | false | metrics-service continues if ADOT fails |
| Health Check | :13133 |
ADOT health extension |
| Dependency | metrics-service HEALTHY | Waits for metrics-service to start |
Configuration (embedded YAML via AOT_CONFIG_CONTENT env var):
receivers:
prometheus:
config:
scrape_configs:
- job_name: 'mcp-metrics-service'
scrape_interval: 15s
static_configs:
- targets: ['localhost:9465']
exporters:
prometheusremotewrite:
endpoint: https://aps-workspaces.<region>.amazonaws.com/workspaces/<id>/api/v1/remote_write
auth:
authenticator: sigv4auth
extensions:
sigv4auth:
region: <region>
health_check:
endpoint: 0.0.0.0:13133
service:
extensions: [sigv4auth, health_check]
pipelines:
metrics:
receivers: [prometheus]
exporters: [prometheusremotewrite]
Grafana OSS
Pre-configured Grafana container:
| Configuration | Value |
|---|---|
| Image | var.grafana_image_uri |
| CPU | 512 |
| Memory | 1024 |
| Port | 3000 |
| Root URL | /grafana/ |
| Auth | Login required (admin / grafana_admin_password) |
| ALB Path | /grafana/* |
Note: Anonymous access is disabled by default. The admin password is configured via grafana_admin_password in terraform.tfvars (marked as sensitive to prevent exposure in plan output). Generate a strong random password with: python3 -c "import secrets; print(secrets.token_urlsafe(24))"
Critical Environment Variables for SigV4 Authentication:
| Variable | Value | Purpose |
|---|---|---|
AWS_REGION |
<deployment region> |
AWS region for SDK |
GF_AUTH_SIGV4_AUTH_ENABLED |
true |
Enables SigV4 signing for AWS datasources |
GF_AWS_ALLOWED_AUTH_PROVIDERS |
default,ec2_iam_role |
Allows ECS task role credential chain |
Without GF_AUTH_SIGV4_AUTH_ENABLED=true, Grafana will not sign requests to AMP even if sigV4Auth: true is set in the datasource configuration. Without GF_AWS_ALLOWED_AUTH_PROVIDERS, Grafana on ECS Fargate will reject the task role credentials. Both are required.
Datasource (provisioned):
- Amazon Managed Prometheus -- Default datasource, SigV4 auth via IAM task role
Dashboard (provisioned):
- MCP Analytics Comprehensive -- 19 panels covering MCP protocol metrics (see "Grafana Dashboard Panels" below)
Prometheus Alert Rules
Three alert rules are configured in the AMP workspace:
| Alert | Condition | Duration |
|---|---|---|
| MCPHighErrorRate | Error rate > 10% | 5 minutes |
| MCPRegistryDown | No requests received | 5 minutes |
| MCPHighLatency | P95 latency > 5 seconds | 5 minutes |
Terraform Configuration
Enabling Observability (default)
enable_observability = true
metrics_service_image_uri = "<account>.dkr.ecr.<region>.amazonaws.com/mcp-gateway-metrics-service:latest"
grafana_image_uri = "<account>.dkr.ecr.<region>.amazonaws.com/mcp-gateway-grafana:latest"
Disabling Observability
enable_observability = false
# No image URIs needed -- all observability resources are skipped
When enable_observability = false:
- Zero observability resources are created
- No AMP workspace, no metrics-service, no ADOT, no Grafana
- Registry and auth-server deploy without
METRICS_SERVICE_URLorMETRICS_API_KEY - No cost impact from observability
- Existing functionality is completely unaffected
Resource Gating
All observability resources use count = var.enable_observability ? 1 : 0:
| Resource | File |
|---|---|
aws_prometheus_workspace.mcp |
observability.tf |
module.ecs_service_metrics |
observability.tf |
aws_iam_policy.adot_amp_write |
observability.tf |
aws_iam_policy.grafana_amp_query |
observability.tf |
aws_lb_target_group.grafana |
observability.tf |
aws_lb_listener_rule.grafana |
observability.tf |
aws_lb_listener_rule.grafana_https |
observability.tf (also gated by enable_https) |
module.ecs_service_grafana |
observability.tf |
random_password.metrics_api_key |
secrets.tf |
aws_secretsmanager_secret.metrics_api_key |
secrets.tf |
Conditional references in ecs-services.tf for the registry and auth-server environment variables use the same gate to avoid referencing resources that do not exist when observability is disabled.
Grafana Dashboard Panels
The pre-provisioned "MCP Gateway - Analytics Dashboard" contains 19 panels:
| Panel | Type | Description |
|---|---|---|
| Real-time Protocol Activity | timeseries | Live MCP request/response volume |
| Authentication Flow Analysis | timeseries | Auth method breakdown over time |
| Authentication Success Rate | stat | Current auth success percentage |
| Active MCP Servers | stat | Count of registered, enabled servers |
| Tool Executions per Hour | stat | Aggregate tool call volume |
| Most Popular Tool | stat | Highest-traffic tool name |
| MCP Latency P95 (by Server & Method) | timeseries | Tail latency per server and method |
| Request Volume Over Time | timeseries | Total request throughput |
| Error Rate Analysis | timeseries | Error percentage with threshold |
| Average Response Times | timeseries | Mean latency trends |
| Server Performance Dashboard | table | Per-server request counts, error rates, avg latency |
| Tool Usage Rankings | table | Most-called tools across all servers |
| MCP Protocol Methods Distribution | bargauge | Breakdown by MCP method type |
| Tool Usage by Call Count | barchart | Tool call volume comparison |
| Client Applications Distribution | bargauge | Traffic by MCP client |
| MCP Protocol Flow Analysis | table | Protocol step timing |
| Authentication Methods Distribution | bargauge | Auth method usage |
| Tool Execution Success Rate | timeseries | Success/failure ratio over time |
| Session Activity by Client | bargauge | Session counts per client |
Metrics begin appearing within 1-2 minutes of the first MCP request passing through the gateway.
Metric Types Collected
Authentication Metrics
mcp_auth_requests_total-- Counter by success, method, servermcp_auth_request_duration_seconds-- Histogram of auth latency
Tool Execution Metrics
mcp_tool_executions_total-- Counter by tool, server, successmcp_tool_execution_duration_seconds-- Histogram of execution time
Discovery Metrics
mcp_tool_discovery_total-- Counter of semantic search requestsmcp_discovery_duration_seconds-- Histogram of search latency
Protocol Flow Metrics
mcp_protocol_latency_seconds-- Time between protocol steps- initialize -> tools/list
- tools/list -> tools/call
- initialize -> tools/call (full flow)
Histogram Bucket Boundaries
The default OTel SDK bucket boundaries have a smallest non-zero boundary of 5 seconds. Since most MCP responses are sub-second, histogram_quantile(0.95, ...) interpolates within the 0-5s bucket and reports misleading values (e.g., ~4.75s P95 for a 50ms response).
This deployment configures ExplicitBucketHistogramAggregation with boundaries from 5ms to 300s:
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0, 30.0, 60.0, 120.0, 300.0
Configurable via the HISTOGRAM_BUCKET_BOUNDARIES environment variable on the metrics-service.
Security Considerations
Network Security
- metrics-service is deployed in private subnets
- Only accessible via Service Connect (internal) or from registry/auth-server security groups
- ADOT sidecar communicates with metrics-service on localhost (no network hop)
- Grafana is exposed via ALB path
/grafana/*behind CloudFront (when enabled) - No direct public internet exposure for metrics-service or ADOT
Authentication
- Service-to-metrics-service: API key authentication (auto-generated, stored in Secrets Manager)
- ADOT-to-AMP: IAM task role with SigV4
- Grafana-to-AMP: IAM task role with SigV4
- User-to-Grafana: Login required (admin /
grafana_admin_passwordfromterraform.tfvars)
IAM Roles
metrics-service Task Role:
SecretsManagerAccess-- read metrics API keyEcsExecTask-- ECS Exec for debuggingAMPRemoteWrite-- ADOT sidecar writes to AMP
Grafana Task Role:
{
"Effect": "Allow",
"Action": [
"aps:QueryMetrics",
"aps:GetMetricMetadata",
"aps:GetSeries",
"aps:GetLabels"
],
"Resource": "arn:aws:aps:<region>:<account>:workspace/<workspace-id>"
}
Cost Considerations
| Component | Estimated Monthly Cost | Notes |
|---|---|---|
| AMP | $0.90/10M samples ingested | ~$5-10/month typical |
| metrics-service + ADOT (Fargate) | ~$15/month | 512 CPU, 1024 MB (shared task) |
| Grafana OSS (Fargate) | ~$15/month | 512 CPU, 1024 MB |
| Secrets Manager | ~$0.40/month | 1 secret |
| Total | ~$35-40/month | For full observability stack |
Setting enable_observability = false reduces this to $0.
Differences from CloudFormation Deployment
| Aspect | CloudFormation | Terraform |
|---|---|---|
| ADOT deployment | Standalone ECS service | Sidecar in metrics-service task |
| ADOT scrape target | metrics-service.internal:9465 |
localhost:9465 |
| Service discovery | DNS-type Cloud Map (Route53 A records) | HTTP-type Cloud Map (no Route53) |
| API key management | CloudFormation parameter (static default) | Secrets Manager (auto-generated) |
| Resource gating | Separate nested stack | count on each resource |
| Grafana datasources | AMP + CloudWatch | AMP only |
| Grafana dashboards | MCP Analytics + AWS Infrastructure | MCP Analytics |
The sidecar pattern used in Terraform is a direct consequence of the HTTP-type Cloud Map limitation. See issue #496 for details.