div18 commited on
Commit
923f89f
·
1 Parent(s): 5a30495

chore(kubernetes): support unbounded max replicas and enhance local setup

Browse files

- Allow ANTIATROPOS_MAX_REPLICAS to be empty or 'unbounded' for no scale-up limit
- Adjust scaling logic to handle unbounded max replicas correctly
- Update .env.example with local defaults and new Prometheus queries
- Simplify start-grafana.ps1 for local Grafana setup without AWS credentials
- Add kind cluster config with max pods set to 250 for local testing
- Enhance local-laptop.yaml with prometheus metrics exposition and python http server
- Update LOCAL_LAPTOP_FASTAPI_GUIDE.md to document unbounded max replicas and setup changes

.env.example CHANGED
@@ -1,34 +1,40 @@
1
  # AntiAtropos runtime defaults (copy to .env and adjust values)
2
 
3
  # Environment mode for FastAPI runtime
4
- ANTIATROPOS_ENV_MODE=aws
5
 
6
  # Reward output to agent
7
  ANTIATROPOS_REWARD_OUTPUT_MODE=normalized
8
 
9
- # Prometheus/AMP endpoint (workspace URL)
10
- PROMETHEUS_URL=https://aps-workspaces.ap-south-1.amazonaws.com/workspaces/REPLACE_WORKSPACE_ID
11
  ANTIATROPOS_PROM_TIMEOUT_S=5.0
12
  ANTIATROPOS_STRICT_REAL=false
13
  ANTIATROPOS_METRIC_AGGREGATION=sum
14
 
15
  # Kubernetes execution settings
16
- KUBECONFIG=D:/AntiAtropos/deploy/aws/kubeconfig-antiatropos.yaml
17
  ANTIATROPOS_K8S_NAMESPACE=prod-sre
18
  ANTIATROPOS_MIN_REPLICAS=1
19
- ANTIATROPOS_MAX_REPLICAS=6
20
  ANTIATROPOS_SCALE_STEP=3
21
 
22
  # Node -> deployment map used by Kubernetes executor
23
  ANTIATROPOS_WORKLOAD_MAP={"node-0":{"deployment":"payments","namespace":"prod-sre"},"node-1":{"deployment":"checkout","namespace":"prod-sre"},"node-2":{"deployment":"catalog","namespace":"prod-sre"},"node-3":{"deployment":"cart","namespace":"prod-sre"},"node-4":{"deployment":"auth","namespace":"prod-sre"}}
24
 
25
- # AWS defaults used by deployment scripts
26
- AWS_REGION=ap-south-1
27
- ANTIATROPOS_GRAFANA_MODE=external
28
 
29
  # Agent LLM provider (optional): Groq (OpenAI-compatible)
30
  # If GROQ_API_KEY is set and API_BASE_URL is not set, inference.py auto-uses Groq.
31
  GROQ_API_KEY=
32
  MODEL_NAME=llama-3.1-8b-instant
33
  ENV_URL=http://localhost:8000
34
- ANTIATROPOS_MODE=aws
 
 
 
 
 
 
 
1
  # AntiAtropos runtime defaults (copy to .env and adjust values)
2
 
3
  # Environment mode for FastAPI runtime
4
+ ANTIATROPOS_ENV_MODE=live
5
 
6
  # Reward output to agent
7
  ANTIATROPOS_REWARD_OUTPUT_MODE=normalized
8
 
9
+ # Local Prometheus endpoint (use kubectl port-forward to localhost:9090)
10
+ PROMETHEUS_URL=http://localhost:9090
11
  ANTIATROPOS_PROM_TIMEOUT_S=5.0
12
  ANTIATROPOS_STRICT_REAL=false
13
  ANTIATROPOS_METRIC_AGGREGATION=sum
14
 
15
  # Kubernetes execution settings
16
+ KUBECONFIG=C:/Users/your-user/.kube/config
17
  ANTIATROPOS_K8S_NAMESPACE=prod-sre
18
  ANTIATROPOS_MIN_REPLICAS=1
19
+ ANTIATROPOS_MAX_REPLICAS=
20
  ANTIATROPOS_SCALE_STEP=3
21
 
22
  # Node -> deployment map used by Kubernetes executor
23
  ANTIATROPOS_WORKLOAD_MAP={"node-0":{"deployment":"payments","namespace":"prod-sre"},"node-1":{"deployment":"checkout","namespace":"prod-sre"},"node-2":{"deployment":"catalog","namespace":"prod-sre"},"node-3":{"deployment":"cart","namespace":"prod-sre"},"node-4":{"deployment":"auth","namespace":"prod-sre"}}
24
 
25
+ # Local defaults
26
+ AWS_REGION=
27
+ ANTIATROPOS_GRAFANA_MODE=local
28
 
29
  # Agent LLM provider (optional): Groq (OpenAI-compatible)
30
  # If GROQ_API_KEY is set and API_BASE_URL is not set, inference.py auto-uses Groq.
31
  GROQ_API_KEY=
32
  MODEL_NAME=llama-3.1-8b-instant
33
  ENV_URL=http://localhost:8000
34
+ ANTIATROPOS_MODE=live
35
+ ANTIATROPOS_LABEL_NODE_MAP={"payments":"node-0","checkout":"node-1","catalog":"node-2","cart":"node-3","auth":"node-4"}
36
+ ANTIATROPOS_PROM_QUERY_REQUEST_RATE=sum(rate(http_requests_total[1m])) by (node_id)
37
+ ANTIATROPOS_PROM_QUERY_LATENCY_MS=histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[1m])) by (node_id, le)) * 1000
38
+ ANTIATROPOS_PROM_QUERY_ERROR_RATE=sum(rate(http_requests_total{status=~"5.."}[1m])) by (node_id) / clamp_min(sum(rate(http_requests_total[1m])) by (node_id), 1)
39
+ ANTIATROPOS_PROM_QUERY_CPU=avg(rate(container_cpu_usage_seconds_total[1m])) by (node_id)
40
+ ANTIATROPOS_PROM_QUERY_QUEUE_DEPTH=avg(queue_depth) by (node_id)
control/kubernetes_executor.py CHANGED
@@ -18,12 +18,34 @@ class KubernetesExecutor:
18
  self.is_mock = not self.kubeconfig or self.kubeconfig.lower() == "mock"
19
  self.namespace = os.getenv("ANTIATROPOS_K8S_NAMESPACE", "default")
20
  self.min_replicas = int(os.getenv("ANTIATROPOS_MIN_REPLICAS", "1"))
21
- self.max_replicas = int(os.getenv("ANTIATROPOS_MAX_REPLICAS", "20"))
22
  self.scale_step = int(os.getenv("ANTIATROPOS_SCALE_STEP", "3"))
23
  self._apps_v1_api = None
24
  self._node_workload_map = self._load_node_workload_map()
25
  self._live_supported_actions = {"NO_OP", "SCALE_UP", "SCALE_DOWN"}
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  @staticmethod
28
  def _normalize_action_type(action_type) -> str:
29
  if hasattr(action_type, "value"):
@@ -116,14 +138,18 @@ class KubernetesExecutor:
116
  current = int(scale_obj.spec.replicas or self.min_replicas)
117
  delta = max(1, int(float(parameter) * self.scale_step))
118
  if action_type == "SCALE_UP":
119
- desired = min(self.max_replicas, current + delta)
 
 
 
120
  else:
121
  desired = max(self.min_replicas, current - delta)
122
 
123
  if desired == current:
 
124
  return (
125
  f"Ack: {action_type} for {target} - replicas unchanged at {current} "
126
- f"(bounds {self.min_replicas}-{self.max_replicas})"
127
  )
128
 
129
  apps_v1.patch_namespaced_deployment_scale(
 
18
  self.is_mock = not self.kubeconfig or self.kubeconfig.lower() == "mock"
19
  self.namespace = os.getenv("ANTIATROPOS_K8S_NAMESPACE", "default")
20
  self.min_replicas = int(os.getenv("ANTIATROPOS_MIN_REPLICAS", "1"))
21
+ self.max_replicas = self._parse_max_replicas(os.getenv("ANTIATROPOS_MAX_REPLICAS"))
22
  self.scale_step = int(os.getenv("ANTIATROPOS_SCALE_STEP", "3"))
23
  self._apps_v1_api = None
24
  self._node_workload_map = self._load_node_workload_map()
25
  self._live_supported_actions = {"NO_OP", "SCALE_UP", "SCALE_DOWN"}
26
 
27
+ @staticmethod
28
+ def _parse_max_replicas(raw: Optional[str]) -> Optional[int]:
29
+ """
30
+ Parse optional max replicas.
31
+
32
+ Returns:
33
+ - int when a positive explicit cap is provided
34
+ - None when scale-up should be unbounded
35
+ """
36
+ if raw is None:
37
+ return None
38
+ value = str(raw).strip().lower()
39
+ if value in ("", "none", "unbounded", "inf", "infinite"):
40
+ return None
41
+ try:
42
+ parsed = int(value)
43
+ except ValueError:
44
+ return None
45
+ if parsed <= 0:
46
+ return None
47
+ return parsed
48
+
49
  @staticmethod
50
  def _normalize_action_type(action_type) -> str:
51
  if hasattr(action_type, "value"):
 
138
  current = int(scale_obj.spec.replicas or self.min_replicas)
139
  delta = max(1, int(float(parameter) * self.scale_step))
140
  if action_type == "SCALE_UP":
141
+ if self.max_replicas is None:
142
+ desired = current + delta
143
+ else:
144
+ desired = min(self.max_replicas, current + delta)
145
  else:
146
  desired = max(self.min_replicas, current - delta)
147
 
148
  if desired == current:
149
+ upper = "unbounded" if self.max_replicas is None else str(self.max_replicas)
150
  return (
151
  f"Ack: {action_type} for {target} - replicas unchanged at {current} "
152
+ f"(bounds {self.min_replicas}-{upper})"
153
  )
154
 
155
  apps_v1.patch_namespaced_deployment_scale(
deploy/LOCAL_LAPTOP_FASTAPI_GUIDE.md CHANGED
@@ -24,7 +24,7 @@ The controller requires `KUBECONFIG` and `ANTIATROPOS_WORKLOAD_MAP`.
24
  $env:KUBECONFIG = "$HOME/.kube/config"
25
  $env:ANTIATROPOS_K8S_NAMESPACE = "prod-sre"
26
  $env:ANTIATROPOS_MIN_REPLICAS = "1"
27
- $env:ANTIATROPOS_MAX_REPLICAS = "6"
28
  $env:ANTIATROPOS_SCALE_STEP = "3"
29
  $env:ANTIATROPOS_WORKLOAD_MAP = '{"node-0":{"deployment":"payments","namespace":"prod-sre"},"node-1":{"deployment":"checkout","namespace":"prod-sre"},"node-2":{"deployment":"catalog","namespace":"prod-sre"},"node-3":{"deployment":"cart","namespace":"prod-sre"},"node-4":{"deployment":"auth","namespace":"prod-sre"}}'
30
  ```
 
24
  $env:KUBECONFIG = "$HOME/.kube/config"
25
  $env:ANTIATROPOS_K8S_NAMESPACE = "prod-sre"
26
  $env:ANTIATROPOS_MIN_REPLICAS = "1"
27
+ $env:ANTIATROPOS_MAX_REPLICAS = "" # empty => unbounded scale-up
28
  $env:ANTIATROPOS_SCALE_STEP = "3"
29
  $env:ANTIATROPOS_WORKLOAD_MAP = '{"node-0":{"deployment":"payments","namespace":"prod-sre"},"node-1":{"deployment":"checkout","namespace":"prod-sre"},"node-2":{"deployment":"catalog","namespace":"prod-sre"},"node-3":{"deployment":"cart","namespace":"prod-sre"},"node-4":{"deployment":"auth","namespace":"prod-sre"}}'
30
  ```
deploy/grafana/provisioning/dashboards/json/antiatropos-live.json CHANGED
@@ -54,7 +54,7 @@
54
  },
55
  "targets": [
56
  {
57
- "expr": "sum by (action_type, ack_class) (rate(antiatropos_actions_total{task_id=~\"$task\",mode=~\"$mode\"}[1m]))",
58
  "legendFormat": "{{action_type}} {{ack_class}}",
59
  "refId": "A"
60
  }
@@ -261,7 +261,7 @@
261
  },
262
  "targets": [
263
  {
264
- "expr": "histogram_quantile(0.95, sum(rate(antiatropos_executor_latency_ms_bucket{mode=~\"$mode\"}[2m])) by (le, mode))",
265
  "legendFormat": "p95 {{mode}}",
266
  "refId": "A"
267
  },
 
54
  },
55
  "targets": [
56
  {
57
+ "expr": "sum by (action_type, ack_class) (rate(antiatropos_actions_total{task_id=~\"$task\",mode=~\"$mode\"}[5m]))",
58
  "legendFormat": "{{action_type}} {{ack_class}}",
59
  "refId": "A"
60
  }
 
261
  },
262
  "targets": [
263
  {
264
+ "expr": "histogram_quantile(0.95, sum(rate(antiatropos_executor_latency_ms_bucket{mode=~\"$mode\"}[5m])) by (le, mode))",
265
  "legendFormat": "p95 {{mode}}",
266
  "refId": "A"
267
  },
deploy/grafana/provisioning/dashboards/json/antiatropos-overview.json CHANGED
@@ -76,8 +76,8 @@
76
  "targets": [
77
  {
78
  "editorMode": "code",
79
- "expr": "scalar(avg(last_over_time(antiatropos_reward{mode=\"simulated\"}[1m])))",
80
- "legendFormat": "reward (simulated)",
81
  "range": true,
82
  "refId": "A"
83
  }
@@ -143,8 +143,8 @@
143
  "targets": [
144
  {
145
  "editorMode": "code",
146
- "expr": "scalar(avg(last_over_time(antiatropos_total_queue_backlog{mode=\"simulated\"}[1m])))",
147
- "legendFormat": "queue backlog (simulated)",
148
  "range": true,
149
  "refId": "A"
150
  }
@@ -210,8 +210,8 @@
210
  "targets": [
211
  {
212
  "editorMode": "code",
213
- "expr": "scalar(avg(last_over_time(antiatropos_average_latency_norm{mode=\"simulated\"}[1m])))",
214
- "legendFormat": "latency (simulated)",
215
  "range": true,
216
  "refId": "A"
217
  }
@@ -277,8 +277,8 @@
277
  "targets": [
278
  {
279
  "editorMode": "code",
280
- "expr": "scalar(avg(last_over_time(antiatropos_lyapunov_energy{mode=\"simulated\"}[1m])))",
281
- "legendFormat": "lyapunov energy (simulated)",
282
  "range": true,
283
  "refId": "A"
284
  }
@@ -369,14 +369,14 @@
369
  "targets": [
370
  {
371
  "editorMode": "code",
372
- "expr": "antiatropos_reward{mode=\"simulated\"}",
373
  "legendFormat": "reward {{task_id}} ({{mode}})",
374
  "range": true,
375
  "refId": "A"
376
  },
377
  {
378
  "editorMode": "code",
379
- "expr": "antiatropos_lyapunov_energy{mode=\"simulated\"}",
380
  "legendFormat": "lyapunov {{task_id}} ({{mode}})",
381
  "range": true,
382
  "refId": "B"
@@ -468,14 +468,14 @@
468
  "targets": [
469
  {
470
  "editorMode": "code",
471
- "expr": "antiatropos_total_queue_backlog{mode=\"simulated\"}",
472
  "legendFormat": "queue {{task_id}} ({{mode}})",
473
  "range": true,
474
  "refId": "A"
475
  },
476
  {
477
  "editorMode": "code",
478
- "expr": "antiatropos_average_latency_norm{mode=\"simulated\"}",
479
  "legendFormat": "latency {{task_id}} ({{mode}})",
480
  "range": true,
481
  "refId": "B"
@@ -535,14 +535,14 @@
535
  "targets": [
536
  {
537
  "editorMode": "code",
538
- "expr": "sum by (task_id, mode) (rate(antiatropos_steps_total{mode=\"simulated\"}[1m]))",
539
  "legendFormat": "steps/sec {{task_id}} ({{mode}})",
540
  "range": true,
541
  "refId": "A"
542
  },
543
  {
544
  "editorMode": "code",
545
- "expr": "sum by (task_id, mode, action_type) (rate(antiatropos_actions_total{mode=\"simulated\"}[1m]))",
546
  "legendFormat": "actions/sec {{action_type}} ({{task_id}}, {{mode}})",
547
  "range": true,
548
  "refId": "B"
@@ -602,14 +602,14 @@
602
  "targets": [
603
  {
604
  "editorMode": "code",
605
- "expr": "sum by (mode, error_code) (rate(antiatropos_executor_errors_total{mode=\"simulated\"}[5m]))",
606
  "legendFormat": "executor errors {{error_code}} ({{mode}})",
607
  "range": true,
608
  "refId": "A"
609
  },
610
  {
611
  "editorMode": "code",
612
- "expr": "histogram_quantile(0.95, sum(rate(antiatropos_executor_latency_ms_bucket{mode=\"simulated\"}[5m])) by (le, mode))",
613
  "legendFormat": "p95 executor latency {{mode}}",
614
  "range": true,
615
  "refId": "B"
@@ -640,3 +640,8 @@
640
  "version": 2,
641
  "weekStart": ""
642
  }
 
 
 
 
 
 
76
  "targets": [
77
  {
78
  "editorMode": "code",
79
+ "expr": "scalar(avg(last_over_time(antiatropos_reward{mode=~\"live|simulated|hybrid|aws\"}[1m])))",
80
+ "legendFormat": "reward (all modes)",
81
  "range": true,
82
  "refId": "A"
83
  }
 
143
  "targets": [
144
  {
145
  "editorMode": "code",
146
+ "expr": "scalar(avg(last_over_time(antiatropos_total_queue_backlog{mode=~\"live|simulated|hybrid|aws\"}[1m])))",
147
+ "legendFormat": "queue backlog (all modes)",
148
  "range": true,
149
  "refId": "A"
150
  }
 
210
  "targets": [
211
  {
212
  "editorMode": "code",
213
+ "expr": "scalar(avg(last_over_time(antiatropos_average_latency_norm{mode=~\"live|simulated|hybrid|aws\"}[1m])))",
214
+ "legendFormat": "latency (all modes)",
215
  "range": true,
216
  "refId": "A"
217
  }
 
277
  "targets": [
278
  {
279
  "editorMode": "code",
280
+ "expr": "scalar(avg(last_over_time(antiatropos_lyapunov_energy{mode=~\"live|simulated|hybrid|aws\"}[1m])))",
281
+ "legendFormat": "lyapunov energy (all modes)",
282
  "range": true,
283
  "refId": "A"
284
  }
 
369
  "targets": [
370
  {
371
  "editorMode": "code",
372
+ "expr": "antiatropos_reward{mode=~\"live|simulated|hybrid|aws\"}",
373
  "legendFormat": "reward {{task_id}} ({{mode}})",
374
  "range": true,
375
  "refId": "A"
376
  },
377
  {
378
  "editorMode": "code",
379
+ "expr": "antiatropos_lyapunov_energy{mode=~\"live|simulated|hybrid|aws\"}",
380
  "legendFormat": "lyapunov {{task_id}} ({{mode}})",
381
  "range": true,
382
  "refId": "B"
 
468
  "targets": [
469
  {
470
  "editorMode": "code",
471
+ "expr": "antiatropos_total_queue_backlog{mode=~\"live|simulated|hybrid|aws\"}",
472
  "legendFormat": "queue {{task_id}} ({{mode}})",
473
  "range": true,
474
  "refId": "A"
475
  },
476
  {
477
  "editorMode": "code",
478
+ "expr": "antiatropos_average_latency_norm{mode=~\"live|simulated|hybrid|aws\"}",
479
  "legendFormat": "latency {{task_id}} ({{mode}})",
480
  "range": true,
481
  "refId": "B"
 
535
  "targets": [
536
  {
537
  "editorMode": "code",
538
+ "expr": "sum by (task_id, mode) (rate(antiatropos_steps_total{mode=~\"live|simulated|hybrid|aws\"}[1m]))",
539
  "legendFormat": "steps/sec {{task_id}} ({{mode}})",
540
  "range": true,
541
  "refId": "A"
542
  },
543
  {
544
  "editorMode": "code",
545
+ "expr": "sum by (task_id, mode, action_type) (rate(antiatropos_actions_total{mode=~\"live|simulated|hybrid|aws\"}[1m]))",
546
  "legendFormat": "actions/sec {{action_type}} ({{task_id}}, {{mode}})",
547
  "range": true,
548
  "refId": "B"
 
602
  "targets": [
603
  {
604
  "editorMode": "code",
605
+ "expr": "sum by (mode, error_code) (rate(antiatropos_executor_errors_total{mode=~\"live|simulated|hybrid|aws\"}[5m]))",
606
  "legendFormat": "executor errors {{error_code}} ({{mode}})",
607
  "range": true,
608
  "refId": "A"
609
  },
610
  {
611
  "editorMode": "code",
612
+ "expr": "histogram_quantile(0.95, sum(rate(antiatropos_executor_latency_ms_bucket{mode=~\"live|simulated|hybrid|aws\"}[5m])) by (le, mode))",
613
  "legendFormat": "p95 executor latency {{mode}}",
614
  "range": true,
615
  "refId": "B"
 
640
  "version": 2,
641
  "weekStart": ""
642
  }
643
+
644
+
645
+
646
+
647
+
deploy/grafana/provisioning/datasources/prometheus.yaml CHANGED
@@ -5,13 +5,6 @@ datasources:
5
  uid: PBFA97CFB590B2093
6
  type: prometheus
7
  access: proxy
8
- url: https://aps-workspaces.ap-south-1.amazonaws.com/workspaces/ws-ba100a53-b185-4e64-8619-59c2bb3d2fce
9
  isDefault: true
10
  editable: true
11
- jsonData:
12
- sigV4Auth: true
13
- sigV4AuthType: keys
14
- sigV4Region: ap-south-1
15
- secureJsonData:
16
- sigV4AccessKey: "${AWS_ACCESS_KEY_ID}"
17
- sigV4SecretKey: "${AWS_SECRET_ACCESS_KEY}"
 
5
  uid: PBFA97CFB590B2093
6
  type: prometheus
7
  access: proxy
8
+ url: http://host.docker.internal:9090
9
  isDefault: true
10
  editable: true
 
 
 
 
 
 
 
deploy/kind-maxpods-250.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ kind: Cluster
2
+ apiVersion: kind.x-k8s.io/v1alpha4
3
+ name: antiatropos-local
4
+ nodes:
5
+ - role: control-plane
6
+ kubeadmConfigPatches:
7
+ - |
8
+ kind: InitConfiguration
9
+ nodeRegistration:
10
+ kubeletExtraArgs:
11
+ max-pods: "250"
deploy/local-laptop.yaml CHANGED
@@ -17,11 +17,63 @@ spec:
17
  metadata:
18
  labels:
19
  app: auth
 
 
 
 
20
  spec:
21
  containers:
22
  - name: auth
23
- image: alpine:latest
24
- command: ["sleep", "infinity"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  ---
26
  apiVersion: apps/v1
27
  kind: Deployment
@@ -37,11 +89,63 @@ spec:
37
  metadata:
38
  labels:
39
  app: cart
 
 
 
 
40
  spec:
41
  containers:
42
  - name: cart
43
- image: alpine:latest
44
- command: ["sleep", "infinity"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  ---
46
  apiVersion: apps/v1
47
  kind: Deployment
@@ -57,11 +161,63 @@ spec:
57
  metadata:
58
  labels:
59
  app: catalog
 
 
 
 
60
  spec:
61
  containers:
62
  - name: catalog
63
- image: alpine:latest
64
- command: ["sleep", "infinity"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  ---
66
  apiVersion: apps/v1
67
  kind: Deployment
@@ -77,11 +233,63 @@ spec:
77
  metadata:
78
  labels:
79
  app: checkout
 
 
 
 
80
  spec:
81
  containers:
82
  - name: checkout
83
- image: alpine:latest
84
- command: ["sleep", "infinity"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  ---
86
  apiVersion: apps/v1
87
  kind: Deployment
@@ -89,7 +297,7 @@ metadata:
89
  name: payments
90
  namespace: prod-sre
91
  spec:
92
- replicas: 1
93
  selector:
94
  matchLabels:
95
  app: payments
@@ -97,8 +305,61 @@ spec:
97
  metadata:
98
  labels:
99
  app: payments
 
 
 
 
100
  spec:
101
  containers:
102
  - name: payments
103
- image: alpine:latest
104
- command: ["sleep", "infinity"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  metadata:
18
  labels:
19
  app: auth
20
+ annotations:
21
+ prometheus.io/scrape: "true"
22
+ prometheus.io/port: "8080"
23
+ prometheus.io/path: "/metrics"
24
  spec:
25
  containers:
26
  - name: auth
27
+ image: python:3.12-alpine
28
+ env:
29
+ - name: NODE_ID
30
+ value: node-4
31
+ - name: BASE_QUEUE
32
+ value: "6"
33
+ command: ["/bin/sh", "-lc"]
34
+ args:
35
+ - |
36
+ mkdir -p /www
37
+ echo ok > /www/index.html
38
+ python -m http.server 8080 --directory /www >/tmp/http.log 2>&1 &
39
+ req=0; err=0; cpu_total=0
40
+ while true; do
41
+ t=$(date +%s)
42
+ noise=$((t % 11))
43
+ req=$((req + 30 + noise))
44
+ q=$((BASE_QUEUE + (t % 20) - 10))
45
+ if [ "$q" -lt 0 ]; then q=0; fi
46
+ err=$((err + q / 20))
47
+ cpu_inc=$((10 + q / 10))
48
+ cpu_total=$((cpu_total + cpu_inc))
49
+ lat_ms=$((35 + q * 3))
50
+ b005=$((req / 5)); b01=$((req / 3)); b025=$((req / 2)); b05=$((req * 3 / 4)); b1=$req; b2=$req
51
+ lat_sum=$(awk "BEGIN {printf \"%.3f\", $req * $lat_ms / 1000.0}")
52
+ {
53
+ echo "# HELP http_requests_total Synthetic request counter"
54
+ echo "# TYPE http_requests_total counter"
55
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"200\"} ${req}"
56
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"500\"} ${err}"
57
+ echo "# HELP queue_depth Synthetic queue depth"
58
+ echo "# TYPE queue_depth gauge"
59
+ echo "queue_depth{node_id=\"${NODE_ID}\"} ${q}"
60
+ echo "# HELP container_cpu_usage_seconds_total Synthetic CPU counter"
61
+ echo "# TYPE container_cpu_usage_seconds_total counter"
62
+ echo "container_cpu_usage_seconds_total{node_id=\"${NODE_ID}\"} ${cpu_total}"
63
+ echo "# HELP http_request_duration_seconds Synthetic request duration histogram"
64
+ echo "# TYPE http_request_duration_seconds histogram"
65
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.05\"} ${b005}"
66
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.1\"} ${b01}"
67
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.25\"} ${b025}"
68
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.5\"} ${b05}"
69
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"1\"} ${b1}"
70
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"2\"} ${b2}"
71
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"+Inf\"} ${req}"
72
+ echo "http_request_duration_seconds_count{node_id=\"${NODE_ID}\"} ${req}"
73
+ echo "http_request_duration_seconds_sum{node_id=\"${NODE_ID}\"} ${lat_sum}"
74
+ } > /www/metrics
75
+ sleep 2
76
+ done
77
  ---
78
  apiVersion: apps/v1
79
  kind: Deployment
 
89
  metadata:
90
  labels:
91
  app: cart
92
+ annotations:
93
+ prometheus.io/scrape: "true"
94
+ prometheus.io/port: "8080"
95
+ prometheus.io/path: "/metrics"
96
  spec:
97
  containers:
98
  - name: cart
99
+ image: python:3.12-alpine
100
+ env:
101
+ - name: NODE_ID
102
+ value: node-3
103
+ - name: BASE_QUEUE
104
+ value: "14"
105
+ command: ["/bin/sh", "-lc"]
106
+ args:
107
+ - |
108
+ mkdir -p /www
109
+ echo ok > /www/index.html
110
+ python -m http.server 8080 --directory /www >/tmp/http.log 2>&1 &
111
+ req=0; err=0; cpu_total=0
112
+ while true; do
113
+ t=$(date +%s)
114
+ noise=$((t % 11))
115
+ req=$((req + 30 + noise))
116
+ q=$((BASE_QUEUE + (t % 20) - 10))
117
+ if [ "$q" -lt 0 ]; then q=0; fi
118
+ err=$((err + q / 20))
119
+ cpu_inc=$((10 + q / 10))
120
+ cpu_total=$((cpu_total + cpu_inc))
121
+ lat_ms=$((35 + q * 3))
122
+ b005=$((req / 5)); b01=$((req / 3)); b025=$((req / 2)); b05=$((req * 3 / 4)); b1=$req; b2=$req
123
+ lat_sum=$(awk "BEGIN {printf \"%.3f\", $req * $lat_ms / 1000.0}")
124
+ {
125
+ echo "# HELP http_requests_total Synthetic request counter"
126
+ echo "# TYPE http_requests_total counter"
127
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"200\"} ${req}"
128
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"500\"} ${err}"
129
+ echo "# HELP queue_depth Synthetic queue depth"
130
+ echo "# TYPE queue_depth gauge"
131
+ echo "queue_depth{node_id=\"${NODE_ID}\"} ${q}"
132
+ echo "# HELP container_cpu_usage_seconds_total Synthetic CPU counter"
133
+ echo "# TYPE container_cpu_usage_seconds_total counter"
134
+ echo "container_cpu_usage_seconds_total{node_id=\"${NODE_ID}\"} ${cpu_total}"
135
+ echo "# HELP http_request_duration_seconds Synthetic request duration histogram"
136
+ echo "# TYPE http_request_duration_seconds histogram"
137
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.05\"} ${b005}"
138
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.1\"} ${b01}"
139
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.25\"} ${b025}"
140
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.5\"} ${b05}"
141
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"1\"} ${b1}"
142
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"2\"} ${b2}"
143
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"+Inf\"} ${req}"
144
+ echo "http_request_duration_seconds_count{node_id=\"${NODE_ID}\"} ${req}"
145
+ echo "http_request_duration_seconds_sum{node_id=\"${NODE_ID}\"} ${lat_sum}"
146
+ } > /www/metrics
147
+ sleep 2
148
+ done
149
  ---
150
  apiVersion: apps/v1
151
  kind: Deployment
 
161
  metadata:
162
  labels:
163
  app: catalog
164
+ annotations:
165
+ prometheus.io/scrape: "true"
166
+ prometheus.io/port: "8080"
167
+ prometheus.io/path: "/metrics"
168
  spec:
169
  containers:
170
  - name: catalog
171
+ image: python:3.12-alpine
172
+ env:
173
+ - name: NODE_ID
174
+ value: node-2
175
+ - name: BASE_QUEUE
176
+ value: "20"
177
+ command: ["/bin/sh", "-lc"]
178
+ args:
179
+ - |
180
+ mkdir -p /www
181
+ echo ok > /www/index.html
182
+ python -m http.server 8080 --directory /www >/tmp/http.log 2>&1 &
183
+ req=0; err=0; cpu_total=0
184
+ while true; do
185
+ t=$(date +%s)
186
+ noise=$((t % 11))
187
+ req=$((req + 30 + noise))
188
+ q=$((BASE_QUEUE + (t % 20) - 10))
189
+ if [ "$q" -lt 0 ]; then q=0; fi
190
+ err=$((err + q / 20))
191
+ cpu_inc=$((10 + q / 10))
192
+ cpu_total=$((cpu_total + cpu_inc))
193
+ lat_ms=$((35 + q * 3))
194
+ b005=$((req / 5)); b01=$((req / 3)); b025=$((req / 2)); b05=$((req * 3 / 4)); b1=$req; b2=$req
195
+ lat_sum=$(awk "BEGIN {printf \"%.3f\", $req * $lat_ms / 1000.0}")
196
+ {
197
+ echo "# HELP http_requests_total Synthetic request counter"
198
+ echo "# TYPE http_requests_total counter"
199
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"200\"} ${req}"
200
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"500\"} ${err}"
201
+ echo "# HELP queue_depth Synthetic queue depth"
202
+ echo "# TYPE queue_depth gauge"
203
+ echo "queue_depth{node_id=\"${NODE_ID}\"} ${q}"
204
+ echo "# HELP container_cpu_usage_seconds_total Synthetic CPU counter"
205
+ echo "# TYPE container_cpu_usage_seconds_total counter"
206
+ echo "container_cpu_usage_seconds_total{node_id=\"${NODE_ID}\"} ${cpu_total}"
207
+ echo "# HELP http_request_duration_seconds Synthetic request duration histogram"
208
+ echo "# TYPE http_request_duration_seconds histogram"
209
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.05\"} ${b005}"
210
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.1\"} ${b01}"
211
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.25\"} ${b025}"
212
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.5\"} ${b05}"
213
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"1\"} ${b1}"
214
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"2\"} ${b2}"
215
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"+Inf\"} ${req}"
216
+ echo "http_request_duration_seconds_count{node_id=\"${NODE_ID}\"} ${req}"
217
+ echo "http_request_duration_seconds_sum{node_id=\"${NODE_ID}\"} ${lat_sum}"
218
+ } > /www/metrics
219
+ sleep 2
220
+ done
221
  ---
222
  apiVersion: apps/v1
223
  kind: Deployment
 
233
  metadata:
234
  labels:
235
  app: checkout
236
+ annotations:
237
+ prometheus.io/scrape: "true"
238
+ prometheus.io/port: "8080"
239
+ prometheus.io/path: "/metrics"
240
  spec:
241
  containers:
242
  - name: checkout
243
+ image: python:3.12-alpine
244
+ env:
245
+ - name: NODE_ID
246
+ value: node-1
247
+ - name: BASE_QUEUE
248
+ value: "24"
249
+ command: ["/bin/sh", "-lc"]
250
+ args:
251
+ - |
252
+ mkdir -p /www
253
+ echo ok > /www/index.html
254
+ python -m http.server 8080 --directory /www >/tmp/http.log 2>&1 &
255
+ req=0; err=0; cpu_total=0
256
+ while true; do
257
+ t=$(date +%s)
258
+ noise=$((t % 11))
259
+ req=$((req + 30 + noise))
260
+ q=$((BASE_QUEUE + (t % 20) - 10))
261
+ if [ "$q" -lt 0 ]; then q=0; fi
262
+ err=$((err + q / 20))
263
+ cpu_inc=$((10 + q / 10))
264
+ cpu_total=$((cpu_total + cpu_inc))
265
+ lat_ms=$((35 + q * 3))
266
+ b005=$((req / 5)); b01=$((req / 3)); b025=$((req / 2)); b05=$((req * 3 / 4)); b1=$req; b2=$req
267
+ lat_sum=$(awk "BEGIN {printf \"%.3f\", $req * $lat_ms / 1000.0}")
268
+ {
269
+ echo "# HELP http_requests_total Synthetic request counter"
270
+ echo "# TYPE http_requests_total counter"
271
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"200\"} ${req}"
272
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"500\"} ${err}"
273
+ echo "# HELP queue_depth Synthetic queue depth"
274
+ echo "# TYPE queue_depth gauge"
275
+ echo "queue_depth{node_id=\"${NODE_ID}\"} ${q}"
276
+ echo "# HELP container_cpu_usage_seconds_total Synthetic CPU counter"
277
+ echo "# TYPE container_cpu_usage_seconds_total counter"
278
+ echo "container_cpu_usage_seconds_total{node_id=\"${NODE_ID}\"} ${cpu_total}"
279
+ echo "# HELP http_request_duration_seconds Synthetic request duration histogram"
280
+ echo "# TYPE http_request_duration_seconds histogram"
281
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.05\"} ${b005}"
282
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.1\"} ${b01}"
283
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.25\"} ${b025}"
284
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.5\"} ${b05}"
285
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"1\"} ${b1}"
286
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"2\"} ${b2}"
287
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"+Inf\"} ${req}"
288
+ echo "http_request_duration_seconds_count{node_id=\"${NODE_ID}\"} ${req}"
289
+ echo "http_request_duration_seconds_sum{node_id=\"${NODE_ID}\"} ${lat_sum}"
290
+ } > /www/metrics
291
+ sleep 2
292
+ done
293
  ---
294
  apiVersion: apps/v1
295
  kind: Deployment
 
297
  name: payments
298
  namespace: prod-sre
299
  spec:
300
+ replicas: 2
301
  selector:
302
  matchLabels:
303
  app: payments
 
305
  metadata:
306
  labels:
307
  app: payments
308
+ annotations:
309
+ prometheus.io/scrape: "true"
310
+ prometheus.io/port: "8080"
311
+ prometheus.io/path: "/metrics"
312
  spec:
313
  containers:
314
  - name: payments
315
+ image: python:3.12-alpine
316
+ env:
317
+ - name: NODE_ID
318
+ value: node-0
319
+ - name: BASE_QUEUE
320
+ value: "30"
321
+ command: ["/bin/sh", "-lc"]
322
+ args:
323
+ - |
324
+ mkdir -p /www
325
+ echo ok > /www/index.html
326
+ python -m http.server 8080 --directory /www >/tmp/http.log 2>&1 &
327
+ req=0; err=0; cpu_total=0
328
+ while true; do
329
+ t=$(date +%s)
330
+ noise=$((t % 11))
331
+ req=$((req + 30 + noise))
332
+ q=$((BASE_QUEUE + (t % 20) - 10))
333
+ if [ "$q" -lt 0 ]; then q=0; fi
334
+ err=$((err + q / 20))
335
+ cpu_inc=$((10 + q / 10))
336
+ cpu_total=$((cpu_total + cpu_inc))
337
+ lat_ms=$((35 + q * 3))
338
+ b005=$((req / 5)); b01=$((req / 3)); b025=$((req / 2)); b05=$((req * 3 / 4)); b1=$req; b2=$req
339
+ lat_sum=$(awk "BEGIN {printf \"%.3f\", $req * $lat_ms / 1000.0}")
340
+ {
341
+ echo "# HELP http_requests_total Synthetic request counter"
342
+ echo "# TYPE http_requests_total counter"
343
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"200\"} ${req}"
344
+ echo "http_requests_total{node_id=\"${NODE_ID}\",status=\"500\"} ${err}"
345
+ echo "# HELP queue_depth Synthetic queue depth"
346
+ echo "# TYPE queue_depth gauge"
347
+ echo "queue_depth{node_id=\"${NODE_ID}\"} ${q}"
348
+ echo "# HELP container_cpu_usage_seconds_total Synthetic CPU counter"
349
+ echo "# TYPE container_cpu_usage_seconds_total counter"
350
+ echo "container_cpu_usage_seconds_total{node_id=\"${NODE_ID}\"} ${cpu_total}"
351
+ echo "# HELP http_request_duration_seconds Synthetic request duration histogram"
352
+ echo "# TYPE http_request_duration_seconds histogram"
353
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.05\"} ${b005}"
354
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.1\"} ${b01}"
355
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.25\"} ${b025}"
356
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"0.5\"} ${b05}"
357
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"1\"} ${b1}"
358
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"2\"} ${b2}"
359
+ echo "http_request_duration_seconds_bucket{node_id=\"${NODE_ID}\",le=\"+Inf\"} ${req}"
360
+ echo "http_request_duration_seconds_count{node_id=\"${NODE_ID}\"} ${req}"
361
+ echo "http_request_duration_seconds_sum{node_id=\"${NODE_ID}\"} ${lat_sum}"
362
+ } > /www/metrics
363
+ sleep 2
364
+ done
365
+
deploy/local/grafana-local-values.yaml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ adminUser: admin
2
+ adminPassword: antiatropos
3
+
4
+ service:
5
+ type: ClusterIP
6
+
7
+ persistence:
8
+ enabled: false
9
+
10
+ resources:
11
+ requests:
12
+ cpu: 100m
13
+ memory: 192Mi
14
+ limits:
15
+ cpu: 400m
16
+ memory: 384Mi
17
+
18
+ datasources:
19
+ datasources.yaml:
20
+ apiVersion: 1
21
+ datasources:
22
+ - name: Prometheus
23
+ type: prometheus
24
+ access: proxy
25
+ url: http://prometheus-server.monitoring.svc.cluster.local
26
+ isDefault: true
27
+ editable: true
28
+
29
+ sidecar:
30
+ dashboards:
31
+ enabled: true
32
+ label: grafana_dashboard
33
+ labelValue: "1"
34
+ searchNamespace: ALL
deploy/local/prometheus-local-values.yaml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ alertmanager:
2
+ enabled: false
3
+
4
+ kube-state-metrics:
5
+ enabled: false
6
+
7
+ prometheus-node-exporter:
8
+ enabled: false
9
+
10
+ prometheus-pushgateway:
11
+ enabled: false
12
+
13
+ extraScrapeConfigs: |
14
+ - job_name: 'antiatropos-fastapi'
15
+ metrics_path: /metrics
16
+ static_configs:
17
+ - targets: ['host.docker.internal:8000']
18
+
19
+ - job_name: 'prod-sre-annotated-pods'
20
+ kubernetes_sd_configs:
21
+ - role: pod
22
+ namespaces:
23
+ names: ['prod-sre']
24
+ relabel_configs:
25
+ - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
26
+ action: keep
27
+ regex: true
28
+ - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
29
+ action: replace
30
+ target_label: __metrics_path__
31
+ regex: (.+)
32
+ - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
33
+ action: replace
34
+ regex: ([^:]+)(?::\d+)?;(\d+)
35
+ replacement: $1:$2
36
+ target_label: __address__
37
+
38
+ server:
39
+ persistentVolume:
40
+ enabled: false
41
+ resources:
42
+ requests:
43
+ cpu: 100m
44
+ memory: 256Mi
45
+ limits:
46
+ cpu: 500m
47
+ memory: 512Mi
48
+ service:
49
+ type: ClusterIP
server/Dockerfile CHANGED
@@ -85,7 +85,7 @@ ENV KUBECONFIG="mock"
85
  ENV ANTIATROPOS_K8S_NAMESPACE="default"
86
  ENV ANTIATROPOS_DEPLOYMENT_PREFIX=""
87
  ENV ANTIATROPOS_MIN_REPLICAS="1"
88
- ENV ANTIATROPOS_MAX_REPLICAS="20"
89
  ENV ANTIATROPOS_SCALE_STEP="3"
90
  ENV ANTIATROPOS_WORKLOAD_MAP="{}"
91
  ENV ANTIATROPOS_NODE_DEPLOYMENT_MAP="{}"
 
85
  ENV ANTIATROPOS_K8S_NAMESPACE="default"
86
  ENV ANTIATROPOS_DEPLOYMENT_PREFIX=""
87
  ENV ANTIATROPOS_MIN_REPLICAS="1"
88
+ ENV ANTIATROPOS_MAX_REPLICAS=""
89
  ENV ANTIATROPOS_SCALE_STEP="3"
90
  ENV ANTIATROPOS_WORKLOAD_MAP="{}"
91
  ENV ANTIATROPOS_NODE_DEPLOYMENT_MAP="{}"
server/app.py CHANGED
@@ -84,6 +84,9 @@ def runtime_config():
84
  except Exception:
85
  mapped_nodes = []
86
 
 
 
 
87
  return {
88
  "env_mode": os.getenv("ANTIATROPOS_ENV_MODE", "simulated"),
89
  "reward_output_mode": os.getenv("ANTIATROPOS_REWARD_OUTPUT_MODE", "normalized"),
@@ -91,7 +94,7 @@ def runtime_config():
91
  "kubeconfig_configured": bool(os.getenv("KUBECONFIG")),
92
  "k8s_namespace": os.getenv("ANTIATROPOS_K8S_NAMESPACE", "default"),
93
  "min_replicas": os.getenv("ANTIATROPOS_MIN_REPLICAS", "1"),
94
- "max_replicas": os.getenv("ANTIATROPOS_MAX_REPLICAS", "20"),
95
  "scale_step": os.getenv("ANTIATROPOS_SCALE_STEP", "3"),
96
  "strict_real": os.getenv("ANTIATROPOS_STRICT_REAL", "false"),
97
  "workload_map_configured": bool(raw_map),
 
84
  except Exception:
85
  mapped_nodes = []
86
 
87
+ raw_max_replicas = os.getenv("ANTIATROPOS_MAX_REPLICAS", "")
88
+ max_replicas_display = raw_max_replicas if raw_max_replicas.strip() else "unbounded"
89
+
90
  return {
91
  "env_mode": os.getenv("ANTIATROPOS_ENV_MODE", "simulated"),
92
  "reward_output_mode": os.getenv("ANTIATROPOS_REWARD_OUTPUT_MODE", "normalized"),
 
94
  "kubeconfig_configured": bool(os.getenv("KUBECONFIG")),
95
  "k8s_namespace": os.getenv("ANTIATROPOS_K8S_NAMESPACE", "default"),
96
  "min_replicas": os.getenv("ANTIATROPOS_MIN_REPLICAS", "1"),
97
+ "max_replicas": max_replicas_display,
98
  "scale_step": os.getenv("ANTIATROPOS_SCALE_STEP", "3"),
99
  "strict_real": os.getenv("ANTIATROPOS_STRICT_REAL", "false"),
100
  "workload_map_configured": bool(raw_map),
start-grafana.ps1 CHANGED
@@ -1,29 +1,12 @@
1
  docker stop antiatropos-grafana 2>$null
2
  docker rm antiatropos-grafana 2>$null
3
 
4
- Write-Host "Fetching AWS credentials..."
5
- $AccessKey = (aws configure get aws_access_key_id).Trim()
6
- $SecretKey = (aws configure get aws_secret_access_key).Trim()
7
- $SessionToken = aws configure get aws_session_token
8
- if ($SessionToken) { $SessionToken = $SessionToken.Trim() }
9
 
10
- Write-Host "Starting Grafana with injected AWS credentials..."
 
 
 
 
11
 
12
- $DockerCmd = "docker run -d --name antiatropos-grafana -p 3000:3000 " + `
13
- "-v ""$PWD\deploy\grafana\provisioning:/etc/grafana/provisioning:ro"" " + `
14
- "-e GF_AUTH_ANONYMOUS_ENABLED=true " + `
15
- "-e GF_AUTH_ANONYMOUS_ORG_ROLE=Admin " + `
16
- "-e GF_AUTH_SIGV4_AUTH_ENABLED=true " + `
17
- "-e AWS_ACCESS_KEY_ID=""$AccessKey"" " + `
18
- "-e AWS_SECRET_ACCESS_KEY=""$SecretKey"" " + `
19
- "-e AWS_REGION=ap-south-1 "
20
-
21
- if ($SessionToken) {
22
- $DockerCmd += "-e AWS_SESSION_TOKEN=""$SessionToken"" "
23
- }
24
-
25
- $DockerCmd += "grafana/grafana:latest"
26
-
27
- Invoke-Expression $DockerCmd
28
-
29
- Write-Host "Grafana is running! Open http://localhost:3000 in your browser."
 
1
  docker stop antiatropos-grafana 2>$null
2
  docker rm antiatropos-grafana 2>$null
3
 
4
+ Write-Host "Starting local Grafana (datasource -> host.docker.internal:9090)..."
 
 
 
 
5
 
6
+ docker run -d --name antiatropos-grafana -p 3000:3000 `
7
+ -v "$PWD\deploy\grafana\provisioning:/etc/grafana/provisioning:ro" `
8
+ -e GF_AUTH_ANONYMOUS_ENABLED=true `
9
+ -e GF_AUTH_ANONYMOUS_ORG_ROLE=Admin `
10
+ grafana/grafana:latest | Out-Null
11
 
12
+ Write-Host "Grafana is running at http://localhost:3000"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
telemetry/mapping.py CHANGED
@@ -25,9 +25,11 @@ class MetricMapper:
25
 
26
  # Safe default mapping for local demos.
27
  return {
28
- "web-node-1": "node-0",
29
- "api-node-2": "node-1",
30
- "db-node-3": "node-2"
 
 
31
  }
32
 
33
  def _resolve_node_id(self, labels: Dict[str, Any]) -> Optional[str]:
 
25
 
26
  # Safe default mapping for local demos.
27
  return {
28
+ "payments": "node-0",
29
+ "checkout": "node-1",
30
+ "catalog": "node-2",
31
+ "cart": "node-3",
32
+ "auth": "node-4",
33
  }
34
 
35
  def _resolve_node_id(self, labels: Dict[str, Any]) -> Optional[str]:
telemetry/prometheus_client.py CHANGED
@@ -1,10 +1,13 @@
1
  import os
2
  import random
 
3
  from typing import Any, Dict, List, Optional
4
  import requests
5
  from pydantic import BaseModel
6
  from .mapping import MetricMapper
7
 
 
 
8
  class TelemetryRecord(BaseModel):
9
  node_id: str
10
  latency_ms: float
@@ -49,7 +52,7 @@ class PrometheusClient:
49
  'sum(queue_depth) by (pod)'
50
  )
51
 
52
- def fetch_latest_metrics(self, node_ids: List[str]) -> Dict[str, TelemetryRecord]:
53
  """
54
  Query Prometheus for the latest metrics for the given nodes.
55
  Returns a mapping from node_id to TelemetryRecord.
@@ -65,9 +68,9 @@ class PrometheusClient:
65
  raise
66
  return self._generate_mock_metrics(node_ids)
67
 
68
- def _fetch_real_metrics(self, node_ids: List[str]) -> Dict[str, TelemetryRecord]:
69
  """Fetches node telemetry from Prometheus instant queries."""
70
- metrics: Dict[str, TelemetryRecord] = {}
71
  saw_any_real_signal = False
72
 
73
  req_by_node = self._collect_metric_values("request_rate", self.request_rate_query, node_ids)
@@ -85,19 +88,35 @@ class PrometheusClient:
85
 
86
  if any(v is not None for v in (req_rate, lat_ms, err_rate, cpu, q_depth)):
87
  saw_any_real_signal = True
 
 
 
 
 
88
 
89
- metrics[node_id] = TelemetryRecord(
90
- node_id=node_id,
91
- latency_ms=float(lat_ms if lat_ms is not None else 20.0),
92
- request_rate=float(req_rate if req_rate is not None else 0.0),
93
- error_rate=max(0.0, min(1.0, float(err_rate if err_rate is not None else 0.0))),
94
- cpu_utilization=max(0.0, min(1.0, float(cpu if cpu is not None else 0.0))),
95
- queue_depth=max(0.0, float(q_depth if q_depth is not None else 0.0)),
96
- )
 
 
 
 
 
97
 
98
  if self.strict_real and not saw_any_real_signal:
99
  raise RuntimeError("Prometheus returned no usable real telemetry for requested node IDs.")
100
 
 
 
 
 
 
 
101
  return metrics
102
 
103
  def _collect_metric_values(
 
1
  import os
2
  import random
3
+ import logging
4
  from typing import Any, Dict, List, Optional
5
  import requests
6
  from pydantic import BaseModel
7
  from .mapping import MetricMapper
8
 
9
+ logger = logging.getLogger("antiatropos.telemetry")
10
+
11
  class TelemetryRecord(BaseModel):
12
  node_id: str
13
  latency_ms: float
 
52
  'sum(queue_depth) by (pod)'
53
  )
54
 
55
+ def fetch_latest_metrics(self, node_ids: List[str]) -> Dict[str, Any]:
56
  """
57
  Query Prometheus for the latest metrics for the given nodes.
58
  Returns a mapping from node_id to TelemetryRecord.
 
68
  raise
69
  return self._generate_mock_metrics(node_ids)
70
 
71
+ def _fetch_real_metrics(self, node_ids: List[str]) -> Dict[str, Any]:
72
  """Fetches node telemetry from Prometheus instant queries."""
73
+ metrics: Dict[str, Any] = {}
74
  saw_any_real_signal = False
75
 
76
  req_by_node = self._collect_metric_values("request_rate", self.request_rate_query, node_ids)
 
88
 
89
  if any(v is not None for v in (req_rate, lat_ms, err_rate, cpu, q_depth)):
90
  saw_any_real_signal = True
91
+ else:
92
+ # No usable sample for this node this cycle; skip reconciliation
93
+ # so simulator dynamics are preserved instead of being collapsed
94
+ # toward zero by synthetic defaults.
95
+ continue
96
 
97
+ node_payload: Dict[str, float] = {}
98
+ if lat_ms is not None:
99
+ node_payload["latency_ms"] = float(lat_ms)
100
+ if req_rate is not None:
101
+ node_payload["request_rate"] = float(req_rate)
102
+ if err_rate is not None:
103
+ node_payload["error_rate"] = max(0.0, min(1.0, float(err_rate)))
104
+ if cpu is not None:
105
+ node_payload["cpu_utilization"] = max(0.0, min(1.0, float(cpu)))
106
+ if q_depth is not None:
107
+ node_payload["queue_depth"] = max(0.0, float(q_depth))
108
+ if node_payload:
109
+ metrics[node_id] = node_payload
110
 
111
  if self.strict_real and not saw_any_real_signal:
112
  raise RuntimeError("Prometheus returned no usable real telemetry for requested node IDs.")
113
 
114
+ if not saw_any_real_signal:
115
+ logger.warning(
116
+ "No per-node Prometheus samples found for configured queries; "
117
+ "skipping telemetry reconciliation for this step."
118
+ )
119
+
120
  return metrics
121
 
122
  def _collect_metric_values(