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.
- awslabs/__init__.py +17 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/__init__.py +17 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/audit_presentation_utils.py +288 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/audit_utils.py +912 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/aws_clients.py +120 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/canary_utils.py +910 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-dotnet-enablement.md +435 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-java-enablement.md +321 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-nodejs-enablement.md +420 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ec2/ec2-python-enablement.md +598 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-dotnet-enablement.md +264 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-java-enablement.md +193 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-nodejs-enablement.md +198 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/ecs/ecs-python-enablement.md +236 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-dotnet-enablement.md +166 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-java-enablement.md +166 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-nodejs-enablement.md +166 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/eks/eks-python-enablement.md +169 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-dotnet-enablement.md +336 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-java-enablement.md +336 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-nodejs-enablement.md +336 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_guides/templates/lambda/lambda-python-enablement.md +336 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/enablement_tools.py +147 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/server.py +1505 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/service_audit_utils.py +231 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/service_tools.py +659 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/sli_report_client.py +333 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/slo_tools.py +386 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/trace_tools.py +784 -0
- awslabs/cloudwatch_applicationsignals_mcp_server/utils.py +172 -0
- awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/METADATA +808 -0
- awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/RECORD +36 -0
- awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/WHEEL +4 -0
- awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/entry_points.txt +2 -0
- awslabs_cloudwatch_applicationsignals_mcp_server-0.1.21.dist-info/licenses/LICENSE +174 -0
- 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)}'
|