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.
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/PKG-INFO +12 -1
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/README.md +11 -0
- newrelic_lambda_cli-0.9.14/newrelic_lambda_cli/apm.py +346 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/__init__.py +2 -0
- newrelic_lambda_cli-0.9.14/newrelic_lambda_cli/cli/apm.py +121 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/layers.py +22 -3
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/types.py +15 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/PKG-INFO +12 -1
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/SOURCES.txt +2 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/setup.py +1 -1
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_layers.py +25 -12
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/CODE_OF_CONDUCT.md +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/CONTRIBUTING.md +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/LICENSE +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/MANIFEST.in +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/THIRD_PARTY_NOTICES.md +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/__init__.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/api.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/decorators.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/functions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/integrations.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/layers.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/otel_ingestions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/subscriptions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cliutils.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/functions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/integrations.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/otel_ingestions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/permissions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/subscriptions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/templates/import-template.yaml +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/templates/license-key-secret.yaml +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/templates/nr-lambda-integration-role.yaml +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/utils.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/dependency_links.txt +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/entry_points.txt +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/not-zip-safe +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/requires.txt +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/top_level.txt +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/pyproject.toml +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/setup.cfg +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_api.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_functions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_integrations.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_new_relic_gql.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_permissions.py +0 -0
- {newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/tests/test_subscriptions.py +0 -0
- {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.
|
|
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
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/__init__.py
RENAMED
|
@@ -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(
|
|
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
|
-
|
|
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)
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: newrelic-lambda-cli
|
|
3
|
-
Version: 0.9.
|
|
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
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -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.
|
|
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,
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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 (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/decorators.py
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/functions.py
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/integrations.py
RENAMED
|
File without changes
|
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/otel_ingestions.py
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/cli/subscriptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/integrations.py
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/otel_ingestions.py
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/permissions.py
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli/subscriptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{newrelic_lambda_cli-0.9.12 → newrelic_lambda_cli-0.9.14}/newrelic_lambda_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|