newrelic-lambda-cli 0.9.12__tar.gz → 0.9.14__tar.gz

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 (48) hide show
  1. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/PKG-INFO +12 -1
  2. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/README.md +11 -0
  3. newrelic_lambda_cli-0.9.14/newrelic_lambda_cli/apm.py +346 -0
  4. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/__init__.py +2 -0
  5. newrelic_lambda_cli-0.9.14/newrelic_lambda_cli/cli/apm.py +121 -0
  6. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/layers.py +22 -3
  7. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/types.py +15 -0
  8. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/PKG-INFO +12 -1
  9. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/SOURCES.txt +2 -0
  10. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/setup.py +1 -1
  11. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_layers.py +25 -12
  12. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/CODE_OF_CONDUCT.md +0 -0
  13. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/CONTRIBUTING.md +0 -0
  14. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/LICENSE +0 -0
  15. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/MANIFEST.in +0 -0
  16. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/THIRD_PARTY_NOTICES.md +0 -0
  17. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/__init__.py +0 -0
  18. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/api.py +0 -0
  19. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/decorators.py +0 -0
  20. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/functions.py +0 -0
  21. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/integrations.py +0 -0
  22. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/layers.py +0 -0
  23. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/otel_ingestions.py +0 -0
  24. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/subscriptions.py +0 -0
  25. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cliutils.py +0 -0
  26. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/functions.py +0 -0
  27. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/integrations.py +0 -0
  28. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/otel_ingestions.py +0 -0
  29. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/permissions.py +0 -0
  30. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/subscriptions.py +0 -0
  31. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/templates/import-template.yaml +0 -0
  32. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/templates/license-key-secret.yaml +0 -0
  33. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/templates/nr-lambda-integration-role.yaml +0 -0
  34. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/utils.py +0 -0
  35. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/dependency_links.txt +0 -0
  36. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/entry_points.txt +0 -0
  37. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/not-zip-safe +0 -0
  38. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/requires.txt +0 -0
  39. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/top_level.txt +0 -0
  40. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/pyproject.toml +0 -0
  41. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/setup.cfg +0 -0
  42. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_api.py +0 -0
  43. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_functions.py +0 -0
  44. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_integrations.py +0 -0
  45. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_new_relic_gql.py +0 -0
  46. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_permissions.py +0 -0
  47. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_subscriptions.py +0 -0
  48. {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: newrelic-lambda-cli
3
- Version: 0.9.12
3
+ Version: 0.9.14
4
4
  Summary: A CLI to install the New Relic AWS Lambda integration and layers.
5
5
  Home-page: https://github.com/newrelic/newrelic-lambda-cli
6
6
  Author: New Relic
@@ -281,6 +281,17 @@ newrelic-lambda subscriptions uninstall --function <name or arn>
281
281
  | `--aws-profile` or `-p` | No | The AWS profile to use for this command. Can also use `AWS_PROFILE`. Will also check `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables if not using AWS CLI. |
282
282
  | `--aws-region` or `-r` | No | The AWS region this function is located. Can use `AWS_DEFAULT_REGION` environment variable. Defaults to AWS session region. |
283
283
 
284
+ ### NewRelic APM + Serverless Convergence
285
+
286
+ #### Migrate Alerts from Lambda to APM
287
+
288
+ ```bash
289
+ newrelic-lambda apm alerts-migrate \
290
+ --nr-account-id <account id> \
291
+ --nr-api-key <api key>
292
+ --function <Lambda-function-name>
293
+ ```
294
+
284
295
  ### NewRelic Otel Ingestions Install
285
296
 
286
297
  #### Install Otel Log Ingestion
@@ -255,6 +255,17 @@ newrelic-lambda subscriptions uninstall --function <name or arn>
255
255
  | `--aws-profile` or `-p` | No | The AWS profile to use for this command. Can also use `AWS_PROFILE`. Will also check `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables if not using AWS CLI. |
256
256
  | `--aws-region` or `-r` | No | The AWS region this function is located. Can use `AWS_DEFAULT_REGION` environment variable. Defaults to AWS session region. |
257
257
 
258
+ ### NewRelic APM + Serverless Convergence
259
+
260
+ #### Migrate Alerts from Lambda to APM
261
+
262
+ ```bash
263
+ newrelic-lambda apm alerts-migrate \
264
+ --nr-account-id <account id> \
265
+ --nr-api-key <api key>
266
+ --function <Lambda-function-name>
267
+ ```
268
+
258
269
  ### NewRelic Otel Ingestions Install
259
270
 
260
271
  #### Install Otel Log Ingestion
@@ -0,0 +1,346 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+
5
+ Example usage:
6
+
7
+ >>> from newrelic_lambda_cli.api import NewRelicGQL
8
+ >>> gql = NewRelicGQL("api key here", "account id here")
9
+ >>> gql.get_linked_accounts()
10
+
11
+ """
12
+
13
+ from gql import Client, gql
14
+ from gql.transport.requests import RequestsHTTPTransport
15
+
16
+ import click
17
+ import requests
18
+ import json
19
+
20
+ from newrelic_lambda_cli.cliutils import failure, success
21
+
22
+
23
+ class NRGQL_APM(object):
24
+ def __init__(self, account_id, api_key, region="us"):
25
+ try:
26
+ self.account_id = int(account_id)
27
+ except ValueError:
28
+ raise ValueError("Account ID must be an integer")
29
+
30
+ self.api_key = api_key
31
+
32
+ if region == "us":
33
+ self.url = "https://api.newrelic.com/graphql"
34
+ elif region == "eu":
35
+ self.url = "https://api.eu.newrelic.com/graphql"
36
+ elif region == "staging":
37
+ self.url = "https://staging-api.newrelic.com/graphql"
38
+ else:
39
+ raise ValueError("Region must be one of 'us' or 'eu'")
40
+
41
+ transport = RequestsHTTPTransport(url=self.url, use_json=True)
42
+ transport.headers = {"api-key": self.api_key}
43
+
44
+ try:
45
+ self.client = Client(transport=transport, fetch_schema_from_transport=True)
46
+ except Exception:
47
+ self.client = Client(transport=transport, fetch_schema_from_transport=False)
48
+
49
+ def query(self, query, timeout=None, **variable_values):
50
+ return self.client.execute(
51
+ gql(query), timeout=timeout, variable_values=variable_values or None
52
+ )
53
+
54
+ def get_entity_guids_from_entity_name(self, entity_name) -> dict[str, str]:
55
+ entity_dicts = {}
56
+ res = self.query(
57
+ f"""
58
+ query {{
59
+ actor {{
60
+ entitySearch(query: "name LIKE '{entity_name}' AND accountId = {self.account_id}") {{
61
+ results {{
62
+ entities {{
63
+ guid
64
+ name
65
+ type
66
+ }}
67
+ }}
68
+ }}
69
+ }}
70
+ }}
71
+ """
72
+ )
73
+
74
+ try:
75
+ data = res
76
+ entities = data["actor"]["entitySearch"]["results"]["entities"]
77
+ for entity in entities:
78
+ if entity["name"] == entity_name:
79
+ entity_dicts[entity["type"]] = entity["guid"]
80
+ return entity_dicts
81
+ except (KeyError, TypeError) as e:
82
+ print(f"An error occurred parsing the response: {e}")
83
+ return None
84
+ except Exception as e:
85
+ print(f"An error occurred: {e}")
86
+ return None
87
+
88
+ def get_entity_alert_details(self, entity_guid) -> dict:
89
+ res = self.query(
90
+ f"""
91
+ query {{
92
+ actor {{
93
+ entity(guid: "{entity_guid}") {{
94
+ name
95
+ guid
96
+ reporting
97
+ alertSeverity
98
+ }}
99
+ account(id: {self.account_id}) {{
100
+ alerts {{
101
+ nrqlConditionsSearch(searchCriteria: {{queryLike: "{entity_guid}"}}) {{
102
+ nrqlConditions {{
103
+ id
104
+ name
105
+ enabled
106
+ description
107
+ policyId
108
+ nrql {{
109
+ query
110
+ }}
111
+ terms {{
112
+ operator
113
+ priority
114
+ threshold
115
+ thresholdDuration
116
+ thresholdOccurrences
117
+ }}
118
+ }}
119
+ }}
120
+ }}
121
+ }}
122
+ }}
123
+ }}
124
+ """
125
+ )
126
+
127
+ print(f"Querying alert details for Lambda entity")
128
+
129
+ try:
130
+ data = res
131
+ # Check for GraphQL errors in the response
132
+ if "errors" in data:
133
+ print("Error in GraphQL response:")
134
+ for error in data["errors"]:
135
+ print(f"- {error.get('message')}")
136
+ return None
137
+
138
+ # Extract the entity data from the response
139
+ actor_data = data.get("actor", {})
140
+ entity_data = actor_data.get("entity")
141
+
142
+ if not entity_data:
143
+ print(f"Error: Could not find data for entity with GUID: {entity_guid}")
144
+ return None
145
+
146
+ # Extract alert conditions from the new path and add them to our entity_data dictionary
147
+ conditions_search_result = (
148
+ actor_data.get("account", {})
149
+ .get("alerts", {})
150
+ .get("nrqlConditionsSearch", {})
151
+ )
152
+ entity_data["alertConditions"] = (
153
+ conditions_search_result.get("nrqlConditions", [])
154
+ if conditions_search_result
155
+ else []
156
+ )
157
+
158
+ return entity_data
159
+
160
+ except (KeyError, TypeError) as e:
161
+ print(f"An error occurred parsing the response: {e}")
162
+ return None
163
+ except Exception as e:
164
+ print(f"An error occurred: {e}")
165
+ return None
166
+
167
+ def create_alert_for_new_entity(
168
+ self, lambda_entity_selected_alerts, lambda_entity_guid, apm_entity_guid
169
+ ):
170
+ # Create a list of new NRQL conditions with modified queries
171
+ new_nrql_conditions = []
172
+ for alert_condition in lambda_entity_selected_alerts:
173
+ original_description = alert_condition.get("description") or ""
174
+ new_description = (
175
+ f"{original_description} migrated from Lambda entity".strip()
176
+ )
177
+ alert_query = alert_condition["nrql"]["query"]
178
+ alert_query = create_apm_alert_query(
179
+ alert_query, lambda_entity_guid, apm_entity_guid
180
+ )
181
+ new_condition = {
182
+ "name": alert_condition["name"] + " - apm_migrated",
183
+ "description": new_description,
184
+ "enabled": True,
185
+ "nrql": {"query": alert_query},
186
+ "terms": alert_condition["terms"],
187
+ }
188
+ policy_id = alert_condition.get("policyId")
189
+ new_nrql_conditions.append((new_condition, policy_id))
190
+
191
+ # Process each condition individually
192
+ results = []
193
+ for new_condition, policy_id in new_nrql_conditions:
194
+ # Validate policy_id
195
+ try:
196
+ policy_id_int = int(policy_id)
197
+ except (ValueError, TypeError):
198
+ print(
199
+ f"Error: Invalid policy ID '{policy_id}' for condition '{new_condition['name']}'. Skipping."
200
+ )
201
+ continue
202
+
203
+ # Format terms as proper GraphQL objects
204
+ terms_list = []
205
+ for term in new_condition["terms"]:
206
+ term_str = "{"
207
+ term_str += f"operator: {term['operator']}, "
208
+ term_str += f"priority: {term['priority']}, "
209
+ term_str += f"threshold: {term['threshold']}, "
210
+ term_str += f"thresholdDuration: {term['thresholdDuration']}, "
211
+ term_str += f"thresholdOccurrences: {term['thresholdOccurrences']}"
212
+ term_str += "}"
213
+ terms_list.append(term_str)
214
+
215
+ terms_str = "[" + ", ".join(terms_list) + "]"
216
+
217
+ mutation = f"""
218
+ mutation {{
219
+ alertsNrqlConditionStaticCreate(
220
+ accountId: {self.account_id},
221
+ condition: {{
222
+ name: "{new_condition['name']}"
223
+ description: "{new_condition['description']}"
224
+ enabled: {str(new_condition['enabled']).lower()}
225
+ nrql: {{
226
+ query: "{new_condition['nrql']['query']}"
227
+ }}
228
+ terms: {terms_str}
229
+ }},
230
+ policyId: "{policy_id_int}"
231
+ ) {{
232
+ id
233
+ name
234
+ description
235
+ nrql {{
236
+ query
237
+ }}
238
+ terms {{
239
+ operator
240
+ priority
241
+ threshold
242
+ thresholdDuration
243
+ thresholdOccurrences
244
+ }}
245
+ }}
246
+ }}
247
+ """
248
+
249
+ try:
250
+ res = self.query(mutation)
251
+ # Check for GraphQL errors in the response
252
+ if "errors" in res:
253
+ print("Error in GraphQL response:")
254
+ for error in res["errors"]:
255
+ print(f"- {error.get('message')}")
256
+ continue
257
+
258
+ result = res["alertsNrqlConditionStaticCreate"]
259
+ results.append(result)
260
+ success(f"Successfully migrated to alert: {result['name']}")
261
+
262
+ except (KeyError, TypeError) as e:
263
+ print(f"An error occurred parsing the response: {e}")
264
+ continue
265
+ except Exception as e:
266
+ print(f"An error occurred: {e}")
267
+ continue
268
+
269
+ return results
270
+
271
+
272
+ lambda_entity_alert_metric = {
273
+ "cwBilledDuration": "apm.lambda.transaction.billed_duration",
274
+ "cwDuration": "apm.lambda.transaction.duration",
275
+ "cwInitDuration": "apm.lambda.transaction.init_duration",
276
+ "cwMaxMemoryUsed": "apm.lambda.transaction.max_memory_used",
277
+ "cwMemorySize": "apm.lambda.transaction.memory_size",
278
+ "cloudWatchBilledDuration": "apm.lambda.transaction.billed_duration",
279
+ "cloudWatchDuration": "apm.lambda.transaction.duration",
280
+ "cloudWatchInitDuration": "apm.lambda.transaction.init_duration",
281
+ }
282
+
283
+
284
+ def check_apm_migrated_alerts(lambda_entity_data, apm_entity_data):
285
+ """
286
+ Check which Lambda alerts have not been migrated to APM yet.
287
+ Returns a list of Lambda alert conditions that don't have corresponding APM migrated versions.
288
+ """
289
+ if not lambda_entity_data or "alertConditions" not in lambda_entity_data:
290
+ print("No alert conditions found for Lambda entity.")
291
+ return []
292
+
293
+ if not apm_entity_data or "alertConditions" not in apm_entity_data:
294
+ print("No alert conditions found for APM entity.")
295
+ return []
296
+
297
+ alerts_not_migrated = []
298
+ lambda_alert_conditions = lambda_entity_data["alertConditions"]
299
+ apm_alert_conditions = apm_entity_data["alertConditions"]
300
+ apm_alerts_name = [condition["name"] for condition in apm_alert_conditions]
301
+
302
+ for lambda_condition in lambda_alert_conditions:
303
+ migrated_lambda_alert_name = lambda_condition["name"] + " - apm_migrated"
304
+ if migrated_lambda_alert_name not in apm_alerts_name:
305
+ alerts_not_migrated.append(lambda_condition)
306
+ else:
307
+ print(f"Alert already migrated, skipping: {lambda_condition['name']}")
308
+
309
+ return alerts_not_migrated
310
+
311
+
312
+ def select_lambda_entity_impacted_alerts(entity_data, apm_entity_data=None):
313
+ if not entity_data or "alertConditions" not in entity_data:
314
+ print("No alert conditions found.")
315
+ return []
316
+
317
+ selected_alerts = []
318
+ alert_conditions = entity_data["alertConditions"]
319
+ for condition in alert_conditions:
320
+ alert_query = condition["nrql"]["query"]
321
+ has_lambda_invocation = "AwsLambdaInvocation" in alert_query
322
+ has_cloudwatch_metrics = any(
323
+ metric in alert_query for metric in lambda_entity_alert_metric.keys()
324
+ )
325
+ if has_lambda_invocation and has_cloudwatch_metrics:
326
+ print(f"Selected alert for migration: {condition['name']}")
327
+ selected_alerts.append(condition)
328
+ if apm_entity_data:
329
+ selected_alerts = check_apm_migrated_alerts(
330
+ {"alertConditions": selected_alerts}, apm_entity_data
331
+ )
332
+ if len(selected_alerts) == 0:
333
+ print(
334
+ "No alerts met the migration criteria (must contain 'AwsLambdaInvocation' or CloudWatch metrics)"
335
+ )
336
+ return selected_alerts
337
+
338
+
339
+ def create_apm_alert_query(alert_query, lambda_entity_guid, apm_entity_guid):
340
+ for key, value in lambda_entity_alert_metric.items():
341
+ if key in alert_query:
342
+ alert_query = alert_query.replace(key, value)
343
+ break
344
+ apm_alert_query = alert_query.replace("AwsLambdaInvocation", "Metric")
345
+ apm_alert_query = apm_alert_query.replace(lambda_entity_guid, apm_entity_guid)
346
+ return apm_alert_query
@@ -3,6 +3,7 @@
3
3
  import click
4
4
 
5
5
  from newrelic_lambda_cli.cli import (
6
+ apm,
6
7
  functions,
7
8
  integrations,
8
9
  layers,
@@ -21,6 +22,7 @@ def cli(ctx, verbose):
21
22
 
22
23
 
23
24
  def register_groups(group):
25
+ apm.register(group)
24
26
  functions.register(group)
25
27
  integrations.register(group)
26
28
  otel_ingestions.register(group)
@@ -0,0 +1,121 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import boto3
4
+ import click
5
+
6
+ from newrelic_lambda_cli import api, apm, otel_ingestions, permissions, integrations
7
+ from newrelic_lambda_cli.types import (
8
+ AlertsMigrate,
9
+ )
10
+ from newrelic_lambda_cli.cli.decorators import add_options, AWS_OPTIONS, NR_OPTIONS
11
+ from newrelic_lambda_cli.cliutils import done, failure
12
+
13
+
14
+ @click.group(name="apm")
15
+ def apm_mode_group():
16
+ """Manage New Relic APM Mode of AWS Lambda instrumentation"""
17
+ pass
18
+
19
+
20
+ def register(group):
21
+ group.add_command(apm_mode_group)
22
+ apm_mode_group.add_command(alerts_migrate)
23
+
24
+
25
+ @click.command(name="alerts-migrate")
26
+ @click.option(
27
+ "--nr-account-id",
28
+ "-a",
29
+ envvar="NEW_RELIC_ACCOUNT_ID",
30
+ help="New Relic Account ID",
31
+ metavar="<account_id>",
32
+ required=True,
33
+ type=click.INT,
34
+ )
35
+ @click.option(
36
+ "--nr-api-key",
37
+ "-k",
38
+ envvar="NEW_RELIC_API_KEY",
39
+ help="New Relic User API Key",
40
+ metavar="<key>",
41
+ required=True,
42
+ )
43
+ @click.option(
44
+ "--nr-region",
45
+ default="us",
46
+ envvar="NEW_RELIC_REGION",
47
+ help="New Relic Account Region",
48
+ metavar="<region>",
49
+ show_default=True,
50
+ type=click.Choice(["us", "eu", "staging"]),
51
+ )
52
+ @add_options(AWS_OPTIONS)
53
+ @click.option(
54
+ "function",
55
+ "--function",
56
+ "-f",
57
+ help="AWS Lambda function name or ARN",
58
+ metavar="<arn>",
59
+ multiple=False,
60
+ required=True,
61
+ )
62
+ @click.option(
63
+ "excludes",
64
+ "--exclude",
65
+ "-e",
66
+ help="Functions to exclude (if using 'all, 'installed', 'not-installed aliases)",
67
+ metavar="<name>",
68
+ multiple=True,
69
+ )
70
+ @click.pass_context
71
+ def alerts_migrate(ctx, **kwargs):
72
+ """Migrate New Relic AWS Lambda Alerts to APM mode"""
73
+ input = AlertsMigrate(session=None, verbose=ctx.obj["VERBOSE"], **kwargs)
74
+ input = input._replace(
75
+ session=boto3.Session(
76
+ profile_name=input.aws_profile, region_name=input.aws_region
77
+ )
78
+ )
79
+
80
+ # Validate required parameters
81
+ if not input.nr_api_key:
82
+ failure(
83
+ "New Relic API key is required. Provide it via --nr-api-key or NEW_RELIC_API_KEY environment variable.",
84
+ exit=True,
85
+ )
86
+
87
+ print("Started migration of alerts")
88
+ if ctx.obj["VERBOSE"]:
89
+ print("Will migrate alerts for {}".format(input.function))
90
+ print(f"Using account ID: {input.nr_account_id}")
91
+ print(f"Using region: {input.nr_region}")
92
+
93
+ # setup client
94
+ client = apm.NRGQL_APM(
95
+ account_id=input.nr_account_id,
96
+ api_key=input.nr_api_key,
97
+ region=input.nr_region,
98
+ )
99
+
100
+ print(f"Getting entity GUID for function: {input.function}")
101
+ result = client.get_entity_guids_from_entity_name(input.function)
102
+ lambda_entity_guid = ""
103
+ apm_entity_guid = ""
104
+ for entity_type, entity_guid in result.items():
105
+ if entity_type == "AWSLAMBDAFUNCTION":
106
+ lambda_entity_guid = entity_guid
107
+ if entity_type == "APPLICATION":
108
+ apm_entity_guid = entity_guid
109
+ lambda_entity_data = client.get_entity_alert_details(lambda_entity_guid)
110
+ apm_entity_data = client.get_entity_alert_details(apm_entity_guid)
111
+ lambda_entity_selected_alerts = apm.select_lambda_entity_impacted_alerts(
112
+ lambda_entity_data, apm_entity_data
113
+ )
114
+ if lambda_entity_selected_alerts:
115
+ client.create_alert_for_new_entity(
116
+ lambda_entity_selected_alerts, lambda_entity_guid, apm_entity_guid
117
+ )
118
+ else:
119
+ print(
120
+ "No alerts need to be migrated - all eligible alerts have already been migrated."
121
+ )
@@ -47,7 +47,18 @@ def index(region, runtime, architecture):
47
47
  ]
48
48
 
49
49
 
50
- def layer_selection(available_layers, runtime, architecture):
50
+ def layer_selection(
51
+ available_layers, runtime, architecture, upgrade=False, existing_layer_arn=None
52
+ ):
53
+ if upgrade and existing_layer_arn:
54
+ base_arn = existing_layer_arn.rsplit(":", 1)[0]
55
+
56
+ for i, layer in enumerate(available_layers):
57
+ candidate_arn = layer["LatestMatchingVersion"]["LayerVersionArn"]
58
+ candidate_base_arn = candidate_arn.rsplit(":", 1)[0]
59
+ if candidate_base_arn == base_arn:
60
+ return candidate_arn
61
+
51
62
  if len(available_layers) == 1:
52
63
  return available_layers[0]["LatestMatchingVersion"]["LayerVersionArn"]
53
64
 
@@ -147,8 +158,16 @@ def _add_new_relic(input, config, nr_license_key):
147
158
  % (config["Configuration"]["FunctionArn"], runtime, architecture)
148
159
  )
149
160
  return False
150
-
151
- new_relic_layer = layer_selection(available_layers, runtime, architecture)
161
+ existing_layer_arn = (
162
+ existing_newrelic_layer[0] if existing_newrelic_layer else None
163
+ )
164
+ new_relic_layer = layer_selection(
165
+ available_layers,
166
+ runtime,
167
+ architecture,
168
+ upgrade=input.upgrade,
169
+ existing_layer_arn=existing_layer_arn,
170
+ )
152
171
 
153
172
  update_kwargs = {
154
173
  "FunctionName": config["Configuration"]["FunctionArn"],
@@ -141,6 +141,19 @@ SUBSCRIPTION_INSTALL_KEYS = [
141
141
  "otel",
142
142
  ]
143
143
 
144
+ ALERTS_MIGRATE_KEYS = [
145
+ "session",
146
+ "aws_profile",
147
+ "aws_region",
148
+ "aws_permissions_check",
149
+ "nr_account_id",
150
+ "nr_api_key",
151
+ "nr_region",
152
+ "function",
153
+ "excludes",
154
+ "verbose",
155
+ ]
156
+
144
157
  SUBSCRIPTION_UNINSTALL_KEYS = [
145
158
  "session",
146
159
  "aws_profile",
@@ -166,5 +179,7 @@ OtelIngestionUpdate = namedtuple("OtelIngestionUpdate", OTEL_INGESTION_UPDATE_KE
166
179
  LayerInstall = namedtuple("LayerInstall", LAYER_INSTALL_KEYS)
167
180
  LayerUninstall = namedtuple("LayerUninstall", LAYER_UNINSTALL_KEYS)
168
181
 
182
+ AlertsMigrate = namedtuple("AlertsMigrate", ALERTS_MIGRATE_KEYS)
183
+
169
184
  SubscriptionInstall = namedtuple("SubscriptionInstall", SUBSCRIPTION_INSTALL_KEYS)
170
185
  SubscriptionUninstall = namedtuple("SubscriptionUninstall", SUBSCRIPTION_UNINSTALL_KEYS)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: newrelic-lambda-cli
3
- Version: 0.9.12
3
+ Version: 0.9.14
4
4
  Summary: A CLI to install the New Relic AWS Lambda integration and layers.
5
5
  Home-page: https://github.com/newrelic/newrelic-lambda-cli
6
6
  Author: New Relic
@@ -281,6 +281,17 @@ newrelic-lambda subscriptions uninstall --function <name or arn>
281
281
  | `--aws-profile` or `-p` | No | The AWS profile to use for this command. Can also use `AWS_PROFILE`. Will also check `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables if not using AWS CLI. |
282
282
  | `--aws-region` or `-r` | No | The AWS region this function is located. Can use `AWS_DEFAULT_REGION` environment variable. Defaults to AWS session region. |
283
283
 
284
+ ### NewRelic APM + Serverless Convergence
285
+
286
+ #### Migrate Alerts from Lambda to APM
287
+
288
+ ```bash
289
+ newrelic-lambda apm alerts-migrate \
290
+ --nr-account-id <account id> \
291
+ --nr-api-key <api key>
292
+ --function <Lambda-function-name>
293
+ ```
294
+
284
295
  ### NewRelic Otel Ingestions Install
285
296
 
286
297
  #### Install Otel Log Ingestion
@@ -9,6 +9,7 @@ setup.cfg
9
9
  setup.py
10
10
  newrelic_lambda_cli/__init__.py
11
11
  newrelic_lambda_cli/api.py
12
+ newrelic_lambda_cli/apm.py
12
13
  newrelic_lambda_cli/cliutils.py
13
14
  newrelic_lambda_cli/functions.py
14
15
  newrelic_lambda_cli/integrations.py
@@ -26,6 +27,7 @@ newrelic_lambda_cli.egg-info/not-zip-safe
26
27
  newrelic_lambda_cli.egg-info/requires.txt
27
28
  newrelic_lambda_cli.egg-info/top_level.txt
28
29
  newrelic_lambda_cli/cli/__init__.py
30
+ newrelic_lambda_cli/cli/apm.py
29
31
  newrelic_lambda_cli/cli/decorators.py
30
32
  newrelic_lambda_cli/cli/functions.py
31
33
  newrelic_lambda_cli/cli/integrations.py
@@ -6,7 +6,7 @@ README = open(os.path.join(os.path.dirname(__file__), "README.md"), "r").read()
6
6
 
7
7
  setup(
8
8
  name="newrelic-lambda-cli",
9
- version="0.9.12",
9
+ version="0.9.14",
10
10
  python_requires=">=3.3",
11
11
  description="A CLI to install the New Relic AWS Lambda integration and layers.",
12
12
  long_description=README,
@@ -143,7 +143,11 @@ def test_add_new_relic(aws_credentials, mock_function_config):
143
143
  )
144
144
 
145
145
  layer_selection_mock.assert_called_with(
146
- mock_index.return_value, "java11", "x86_64"
146
+ mock_index.return_value,
147
+ "java11",
148
+ "x86_64",
149
+ upgrade=None,
150
+ existing_layer_arn=None,
147
151
  )
148
152
  assert "original_handler" in config["Configuration"]["Handler"]
149
153
 
@@ -417,7 +421,6 @@ def test_add_new_relic_nodejs(aws_credentials, mock_function_config):
417
421
 
418
422
  runtime = "nodejs20.x"
419
423
 
420
- # --- Scenario 1: Standard Node.js Handler (ESM disabled) ---
421
424
  print(f"\nTesting Node.js ({runtime}) Standard Handler...")
422
425
  original_std_handler = "original_handler"
423
426
  config_std = mock_function_config(runtime)
@@ -430,11 +433,16 @@ def test_add_new_relic_nodejs(aws_credentials, mock_function_config):
430
433
  enable_extension_function_logs=True,
431
434
  )
432
435
 
433
- update_kwargs_std = _add_new_relic(
434
- install_opts_std,
435
- config_std,
436
- nr_license_key=nr_license_key,
437
- )
436
+ with patch("sys.stdout.isatty") as mock_isatty, patch(
437
+ "newrelic_lambda_cli.layers.click.prompt"
438
+ ) as mock_prompt:
439
+ mock_isatty.return_value = True
440
+ mock_prompt.return_value = 0
441
+ update_kwargs_std = _add_new_relic(
442
+ install_opts_std,
443
+ config_std,
444
+ nr_license_key=nr_license_key,
445
+ )
438
446
 
439
447
  assert update_kwargs_std is not False, "Expected update_kwargs, not False"
440
448
  assert (
@@ -479,11 +487,16 @@ def test_add_new_relic_nodejs(aws_credentials, mock_function_config):
479
487
  esm=True,
480
488
  )
481
489
 
482
- update_kwargs_esm = _add_new_relic(
483
- install_opts_esm,
484
- config_esm,
485
- nr_license_key=nr_license_key,
486
- )
490
+ with patch("sys.stdout.isatty") as mock_isatty, patch(
491
+ "newrelic_lambda_cli.layers.click.prompt"
492
+ ) as mock_prompt:
493
+ mock_isatty.return_value = True
494
+ mock_prompt.return_value = 0
495
+ update_kwargs_esm = _add_new_relic(
496
+ install_opts_esm,
497
+ config_esm,
498
+ nr_license_key=nr_license_key,
499
+ )
487
500
 
488
501
  assert update_kwargs_esm is not False, "Expected update_kwargs, not False"
489
502
  assert (