awslabs.cloudwatch-applicationsignals-mcp-server 0.1.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. awslabs/__init__.py +17 -0
  2. awslabs/cloudwatch_applicationsignals_mcp_server/__init__.py +17 -0
  3. awslabs/cloudwatch_applicationsignals_mcp_server/audit_presentation_utils.py +288 -0
  4. awslabs/cloudwatch_applicationsignals_mcp_server/audit_utils.py +912 -0
  5. awslabs/cloudwatch_applicationsignals_mcp_server/aws_clients.py +120 -0
  6. awslabs/cloudwatch_applicationsignals_mcp_server/canary_utils.py +910 -0
  7. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-dotnet-enablement.md +435 -0
  8. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-java-enablement.md +321 -0
  9. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-nodejs-enablement.md +420 -0
  10. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-python-enablement.md +598 -0
  11. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-dotnet-enablement.md +264 -0
  12. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-java-enablement.md +193 -0
  13. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-nodejs-enablement.md +198 -0
  14. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-python-enablement.md +236 -0
  15. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-dotnet-enablement.md +166 -0
  16. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-java-enablement.md +166 -0
  17. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-nodejs-enablement.md +166 -0
  18. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-python-enablement.md +169 -0
  19. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-dotnet-enablement.md +336 -0
  20. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-java-enablement.md +336 -0
  21. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-nodejs-enablement.md +336 -0
  22. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-python-enablement.md +336 -0
  23. awslabs/cloudwatch_applicationsignals_mcp_server/enablement_tools.py +147 -0
  24. awslabs/cloudwatch_applicationsignals_mcp_server/server.py +1505 -0
  25. awslabs/cloudwatch_applicationsignals_mcp_server/service_audit_utils.py +231 -0
  26. awslabs/cloudwatch_applicationsignals_mcp_server/service_tools.py +659 -0
  27. awslabs/cloudwatch_applicationsignals_mcp_server/sli_report_client.py +333 -0
  28. awslabs/cloudwatch_applicationsignals_mcp_server/slo_tools.py +386 -0
  29. awslabs/cloudwatch_applicationsignals_mcp_server/trace_tools.py +784 -0
  30. awslabs/cloudwatch_applicationsignals_mcp_server/utils.py +172 -0
  31. awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/METADATA +808 -0
  32. awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/RECORD +36 -0
  33. awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/WHEEL +4 -0
  34. awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/entry_points.txt +2 -0
  35. awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/licenses/LICENSE +174 -0
  36. awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,386 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """CloudWatch Application Signals MCP Server - SLO-related tools."""
16
+
17
+ import json
18
+ from .aws_clients import applicationsignals_client
19
+ from botocore.exceptions import ClientError
20
+ from loguru import logger
21
+ from pydantic import Field
22
+ from time import perf_counter as timer
23
+
24
+
25
+ async def get_slo(
26
+ slo_id: str = Field(..., description='The ARN or name of the SLO to retrieve'),
27
+ ) -> str:
28
+ """Get detailed information about a specific Service Level Objective (SLO).
29
+
30
+ **RECOMMENDED WORKFLOW AFTER USING THIS TOOL:**
31
+ After getting SLO configuration details, use `audit_slos()` with `auditors="all"` for comprehensive root cause analysis:
32
+ - `audit_slos(slo_targets='[{"Type":"slo","Data":{"Slo":{"SloName":"your-slo-name"}}}]', auditors="all")`
33
+ - This provides deep root cause analysis with traces, logs, metrics, and dependencies
34
+ - Much more comprehensive than using individual trace tools
35
+
36
+ Use this tool to:
37
+ - Get comprehensive SLO configuration details
38
+ - Understand what metrics the SLO monitors
39
+ - See threshold values and comparison operators
40
+ - Extract operation names and key attributes for further investigation
41
+ - Identify dependency configurations
42
+ - Review attainment goals and burn rate settings
43
+
44
+ Returns detailed information including:
45
+ - SLO name, description, and metadata
46
+ - Metric configuration (for period-based or request-based SLOs)
47
+ - Key attributes and operation names
48
+ - Metric type (LATENCY or AVAILABILITY)
49
+ - Threshold values and comparison operators
50
+ - Goal configuration (attainment percentage, time interval)
51
+ - Burn rate configurations
52
+
53
+ This tool is essential for:
54
+ - Understanding SLO configuration before deep investigation
55
+ - Getting the exact SLO name/ARN for use with audit_slos()
56
+ - Identifying the metrics and thresholds being monitored
57
+ - Planning comprehensive root cause analysis workflow
58
+
59
+ **NEXT STEP: Use audit_slos() with auditors="all" for root cause analysis**
60
+ """
61
+ start_time_perf = timer()
62
+ logger.info(f'Starting get_service_level_objective request for SLO: {slo_id}')
63
+
64
+ try:
65
+ response = applicationsignals_client.get_service_level_objective(Id=slo_id)
66
+ slo = response.get('Slo', {})
67
+
68
+ if not slo:
69
+ logger.warning(f'No SLO found with ID: {slo_id}')
70
+ return f'No SLO found with ID: {slo_id}'
71
+
72
+ result = 'Service Level Objective Details\n'
73
+ result += '=' * 50 + '\n\n'
74
+
75
+ # Basic info
76
+ result += f'Name: {slo.get("Name", "Unknown")}\n'
77
+ result += f'ARN: {slo.get("Arn", "Unknown")}\n'
78
+ if slo.get('Description'):
79
+ result += f'Description: {slo.get("Description", "")}\n'
80
+ result += f'Evaluation Type: {slo.get("EvaluationType", "Unknown")}\n'
81
+ result += f'Created: {slo.get("CreatedTime", "Unknown")}\n'
82
+ result += f'Last Updated: {slo.get("LastUpdatedTime", "Unknown")}\n\n'
83
+
84
+ # Goal configuration
85
+ goal = slo.get('Goal', {})
86
+ if goal:
87
+ result += 'Goal Configuration:\n'
88
+ result += f'• Attainment Goal: {goal.get("AttainmentGoal", 99)}%\n'
89
+ result += f'• Warning Threshold: {goal.get("WarningThreshold", 50)}%\n'
90
+
91
+ interval = goal.get('Interval', {})
92
+ if 'RollingInterval' in interval:
93
+ rolling = interval['RollingInterval']
94
+ result += f'• Interval: Rolling {rolling.get("Duration")} {rolling.get("DurationUnit")}\n'
95
+ elif 'CalendarInterval' in interval:
96
+ calendar = interval['CalendarInterval']
97
+ result += f'• Interval: Calendar {calendar.get("Duration")} {calendar.get("DurationUnit")} starting {calendar.get("StartTime")}\n'
98
+ result += '\n'
99
+
100
+ # Period-based SLI
101
+ if 'Sli' in slo:
102
+ sli = slo['Sli']
103
+ result += 'Period-Based SLI Configuration:\n'
104
+
105
+ sli_metric = sli.get('SliMetric', {})
106
+ if sli_metric:
107
+ # Key attributes - crucial for trace queries
108
+ key_attrs = sli_metric.get('KeyAttributes', {})
109
+ if key_attrs:
110
+ result += '• Key Attributes:\n'
111
+ for k, v in key_attrs.items():
112
+ result += f' - {k}: {v}\n'
113
+
114
+ # Operation name - essential for trace filtering
115
+ if sli_metric.get('OperationName'):
116
+ result += f'• Operation Name: {sli_metric.get("OperationName", "")}\n'
117
+ result += f' (Use this in trace queries: annotation[aws.local.operation]="{sli_metric.get("OperationName", "")}")\n'
118
+
119
+ result += f'• Metric Type: {sli_metric.get("MetricType", "Unknown")}\n'
120
+
121
+ # MetricDataQueries - detailed metric configuration
122
+ metric_queries = sli_metric.get('MetricDataQueries', [])
123
+ if metric_queries:
124
+ result += '• Metric Data Queries:\n'
125
+ for query in metric_queries:
126
+ query_id = query.get('Id', 'Unknown')
127
+ result += f' Query ID: {query_id}\n'
128
+
129
+ # MetricStat details
130
+ metric_stat = query.get('MetricStat', {})
131
+ if metric_stat:
132
+ metric = metric_stat.get('Metric', {})
133
+ if metric:
134
+ result += f' Namespace: {metric.get("Namespace", "Unknown")}\n'
135
+ result += (
136
+ f' MetricName: {metric.get("MetricName", "Unknown")}\n'
137
+ )
138
+
139
+ # Dimensions - crucial for understanding what's being measured
140
+ dimensions = metric.get('Dimensions', [])
141
+ if dimensions:
142
+ result += ' Dimensions:\n'
143
+ for dim in dimensions:
144
+ result += f' - {dim.get("Name", "Unknown")}: {dim.get("Value", "Unknown")}\n'
145
+
146
+ result += (
147
+ f' Period: {metric_stat.get("Period", "Unknown")} seconds\n'
148
+ )
149
+ result += f' Stat: {metric_stat.get("Stat", "Unknown")}\n'
150
+ if metric_stat.get('Unit'):
151
+ result += f' Unit: {metric_stat["Unit"]}\n' # type: ignore
152
+
153
+ # Expression if present
154
+ if query.get('Expression'):
155
+ result += f' Expression: {query.get("Expression", "")}\n'
156
+
157
+ result += f' ReturnData: {query.get("ReturnData", True)}\n'
158
+
159
+ # Dependency config
160
+ dep_config = sli_metric.get('DependencyConfig', {})
161
+ if dep_config:
162
+ result += '• Dependency Configuration:\n'
163
+ dep_attrs = dep_config.get('DependencyKeyAttributes', {})
164
+ if dep_attrs:
165
+ result += ' Key Attributes:\n'
166
+ for k, v in dep_attrs.items():
167
+ result += f' - {k}: {v}\n'
168
+ if dep_config.get('DependencyOperationName'):
169
+ result += (
170
+ f' - Dependency Operation: {dep_config["DependencyOperationName"]}\n'
171
+ )
172
+ result += f' (Use in traces: annotation[aws.remote.operation]="{dep_config["DependencyOperationName"]}")\n'
173
+
174
+ result += f'• Threshold: {sli.get("MetricThreshold", "Unknown")}\n'
175
+ result += f'• Comparison: {sli.get("ComparisonOperator", "Unknown")}\n\n'
176
+
177
+ # Request-based SLI
178
+ if 'RequestBasedSli' in slo:
179
+ rbs = slo['RequestBasedSli']
180
+ result += 'Request-Based SLI Configuration:\n'
181
+
182
+ rbs_metric = rbs.get('RequestBasedSliMetric', {})
183
+ if rbs_metric:
184
+ # Key attributes
185
+ key_attrs = rbs_metric.get('KeyAttributes', {})
186
+ if key_attrs:
187
+ result += '• Key Attributes:\n'
188
+ for k, v in key_attrs.items():
189
+ result += f' - {k}: {v}\n'
190
+
191
+ # Operation name
192
+ if rbs_metric.get('OperationName'):
193
+ result += f'• Operation Name: {rbs_metric.get("OperationName", "")}\n'
194
+ result += f' (Use this in trace queries: annotation[aws.local.operation]="{rbs_metric.get("OperationName", "")}")\n'
195
+
196
+ result += f'• Metric Type: {rbs_metric.get("MetricType", "Unknown")}\n'
197
+
198
+ # MetricDataQueries - detailed metric configuration
199
+ metric_queries = rbs_metric.get('MetricDataQueries', [])
200
+ if metric_queries:
201
+ result += '• Metric Data Queries:\n'
202
+ for query in metric_queries:
203
+ query_id = query.get('Id', 'Unknown')
204
+ result += f' Query ID: {query_id}\n'
205
+
206
+ # MetricStat details
207
+ metric_stat = query.get('MetricStat', {})
208
+ if metric_stat:
209
+ metric = metric_stat.get('Metric', {})
210
+ if metric:
211
+ result += f' Namespace: {metric.get("Namespace", "Unknown")}\n'
212
+ result += (
213
+ f' MetricName: {metric.get("MetricName", "Unknown")}\n'
214
+ )
215
+
216
+ # Dimensions - crucial for understanding what's being measured
217
+ dimensions = metric.get('Dimensions', [])
218
+ if dimensions:
219
+ result += ' Dimensions:\n'
220
+ for dim in dimensions:
221
+ result += f' - {dim.get("Name", "Unknown")}: {dim.get("Value", "Unknown")}\n'
222
+
223
+ result += (
224
+ f' Period: {metric_stat.get("Period", "Unknown")} seconds\n'
225
+ )
226
+ result += f' Stat: {metric_stat.get("Stat", "Unknown")}\n'
227
+ if metric_stat.get('Unit'):
228
+ result += f' Unit: {metric_stat["Unit"]}\n' # type: ignore
229
+
230
+ # Expression if present
231
+ if query.get('Expression'):
232
+ result += f' Expression: {query.get("Expression", "")}\n'
233
+
234
+ result += f' ReturnData: {query.get("ReturnData", True)}\n'
235
+
236
+ # Dependency config
237
+ dep_config = rbs_metric.get('DependencyConfig', {})
238
+ if dep_config:
239
+ result += '• Dependency Configuration:\n'
240
+ dep_attrs = dep_config.get('DependencyKeyAttributes', {})
241
+ if dep_attrs:
242
+ result += ' Key Attributes:\n'
243
+ for k, v in dep_attrs.items():
244
+ result += f' - {k}: {v}\n'
245
+ if dep_config.get('DependencyOperationName'):
246
+ result += (
247
+ f' - Dependency Operation: {dep_config["DependencyOperationName"]}\n'
248
+ )
249
+ result += f' (Use in traces: annotation[aws.remote.operation]="{dep_config["DependencyOperationName"]}")\n'
250
+
251
+ result += f'• Threshold: {rbs.get("MetricThreshold", "Unknown")}\n'
252
+ result += f'• Comparison: {rbs.get("ComparisonOperator", "Unknown")}\n\n'
253
+
254
+ # Burn rate configurations
255
+ burn_rates = slo.get('BurnRateConfigurations', [])
256
+ if burn_rates:
257
+ result += 'Burn Rate Configurations:\n'
258
+ for br in burn_rates:
259
+ result += f'• Look-back window: {br.get("LookBackWindowMinutes")} minutes\n'
260
+
261
+ elapsed_time = timer() - start_time_perf
262
+ logger.info(f"get_service_level_objective completed for '{slo_id}' in {elapsed_time:.3f}s")
263
+ return result
264
+
265
+ except ClientError as e:
266
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
267
+ error_message = e.response.get('Error', {}).get('Message', 'Unknown error')
268
+ logger.error(f'AWS ClientError in get_slo: {error_code} - {error_message}')
269
+ return f'AWS Error: {error_message}'
270
+ except Exception as e:
271
+ logger.error(
272
+ f"Unexpected error in get_service_level_objective for '{slo_id}': {str(e)}",
273
+ exc_info=True,
274
+ )
275
+ return f'Error: {str(e)}'
276
+
277
+
278
+ async def list_slos(
279
+ key_attributes: str = Field(
280
+ default='{}',
281
+ description='JSON string of key attributes to filter SLOs (e.g., \'{"Name": "my-service", "Environment": "ecs:my-cluster"}\'. Defaults to empty object to list all SLOs.',
282
+ ),
283
+ include_linked_accounts: bool = Field(
284
+ default=True, description='Whether to include SLOs from linked accounts (default: True)'
285
+ ),
286
+ max_results: int = Field(
287
+ default=50, description='Maximum number of SLOs to return (default: 50, max: 50)'
288
+ ),
289
+ ) -> str:
290
+ """List all Service Level Objectives (SLOs) in Application Signals.
291
+
292
+ Use this tool to:
293
+ - Get a complete list of all SLOs in your account
294
+ - Discover SLO names and ARNs for use with other tools
295
+ - Filter SLOs by service attributes
296
+ - See basic SLO information including creation time and operation names
297
+
298
+ Returns a formatted list showing:
299
+ - SLO name and ARN
300
+ - Associated service key attributes
301
+ - Operation name being monitored
302
+ - Creation timestamp
303
+ - Total count of SLOs found
304
+
305
+ This tool is useful for:
306
+ - SLO discovery and inventory
307
+ - Finding SLO names to use with get_slo() or audit_service_health()
308
+ - Understanding what operations are being monitored
309
+ """
310
+ start_time_perf = timer()
311
+ logger.debug('Starting list_slos request')
312
+
313
+ try:
314
+ # Parse key_attributes JSON string
315
+ try:
316
+ key_attrs_dict = json.loads(key_attributes) if key_attributes else {}
317
+ except json.JSONDecodeError as e:
318
+ return f'Error: Invalid JSON in key_attributes parameter: {str(e)}'
319
+
320
+ # Validate max_results
321
+ max_results = min(max(max_results, 1), 50) # Ensure between 1 and 50
322
+
323
+ # Build request parameters
324
+ request_params = {
325
+ 'MaxResults': max_results,
326
+ 'IncludeLinkedAccounts': include_linked_accounts,
327
+ }
328
+
329
+ # Add key attributes if provided
330
+ if key_attrs_dict:
331
+ request_params['KeyAttributes'] = key_attrs_dict
332
+
333
+ logger.debug(f'Listing SLOs with parameters: {request_params}')
334
+
335
+ # Call the Application Signals API
336
+ response = applicationsignals_client.list_service_level_objectives(**request_params)
337
+ slo_summaries = response.get('SloSummaries', [])
338
+
339
+ logger.debug(f'Retrieved {len(slo_summaries)} SLO summaries')
340
+
341
+ if not slo_summaries:
342
+ logger.info('No SLOs found matching the criteria')
343
+ return 'No Service Level Objectives found matching the specified criteria.'
344
+
345
+ # Build formatted response
346
+ result = f'Service Level Objectives ({len(slo_summaries)} total):\n\n'
347
+
348
+ for slo in slo_summaries:
349
+ slo_name = slo.get('Name', 'Unknown')
350
+ slo_arn = slo.get('Arn', 'Unknown')
351
+ operation_name = slo.get('OperationName', 'N/A')
352
+ created_time = slo.get('CreatedTime', 'Unknown')
353
+
354
+ result += f'• SLO: {slo_name}\n'
355
+ result += f' ARN: {slo_arn}\n'
356
+ result += f' Operation: {operation_name}\n'
357
+ result += f' Created: {created_time}\n'
358
+
359
+ # Add key attributes if available
360
+ key_attrs = slo.get('KeyAttributes', {})
361
+ if key_attrs:
362
+ result += ' Service Attributes:\n'
363
+ for key, value in key_attrs.items():
364
+ result += f' {key}: {value}\n'
365
+
366
+ result += '\n'
367
+
368
+ # Add pagination info if there might be more results
369
+ next_token = response.get('NextToken')
370
+ if next_token:
371
+ result += f'Note: More SLOs may be available. This response shows the first {len(slo_summaries)} results.\n'
372
+
373
+ elapsed_time = timer() - start_time_perf
374
+ logger.debug(
375
+ f'list_slos completed in {elapsed_time:.3f}s - found {len(slo_summaries)} SLOs'
376
+ )
377
+ return result
378
+
379
+ except ClientError as e:
380
+ error_code = e.response.get('Error', {}).get('Code', 'Unknown')
381
+ error_message = e.response.get('Error', {}).get('Message', 'Unknown error')
382
+ logger.error(f'AWS ClientError in list_slos: {error_code} - {error_message}')
383
+ return f'AWS Error: {error_message}'
384
+ except Exception as e:
385
+ logger.error(f'Unexpected error in list_slos: {str(e)}', exc_info=True)
386
+ return f'Error: {str(e)}'