Repository navigation
Expand file tree
/
Copy pathlambda.py
More file actions
322 lines (267 loc) · 11.1 KB
/
Copy pathlambda.py
File metadata and controls
322 lines (267 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
"""
AWS Lambda execution environment wrapper
@author: David Moss, Destry Teeter
@copyright: 2023 People Power Company. All rights reserved.
@contact: dmoss@caredaily.ai, destry@caredaily.ai
"""
import importlib
import json
import sys
import time
import traceback
import botengine as BotEngine # type: ignore
import boto3 # type: ignore
LOG_LEVEL_DEFAULT = "warn" # Default log level
LOG_LEVEL_EVENTS = "info" # Event log level
# AWS Lambda output size limit
OUTPUT_SIZE = 256 * 1024 # 256 KiB
def lambda_handler(data, context):
"""
Execution wrapper on AWS Lambda
:param data: Inputs to the botengine
:param context: Ignored
:return: JSON structure with errors and debug information
"""
if data is None:
# No data to process
logger = LambdaLogger(log_level=LOG_LEVEL_DEFAULT)
logger.exception(
"Missing data inputs for execution: {}".format(json.dumps(context))
)
return logger.get_lambda_return()
# TODO: Allow multiple loggers to differentiate logs from different microservices
logger = LambdaLogger(
log_level=LOG_LEVEL_EVENTS
if data.get("logEvents", False)
else LOG_LEVEL_DEFAULT
)
logger.info(">lambda_handler() api_hosts={}".format(data.get("apiHosts") or data.get("apiHost")))
logger.debug(f"|lambda_handler() data={json.dumps(data)}")
# Get allowable output size
allowed_output_size = OUTPUT_SIZE - len(json.dumps(data).encode("utf-8"))
try:
bot = importlib.import_module("bot")
botengine = BotEngine._run(bot, data, logger, context)
except Exception:
(t, v, tb) = sys.exc_info()
logger.exception(
"Failed to execute bot: {}".format(traceback.format_exception(t, v, tb))
)
logger.info("<lambda_handler()")
return logger.get_lambda_return(allowed_output_size=allowed_output_size)
# Check for asynchronous data request triggers which handle errors differently than synchronous executions of the bot.
if "inputs" in data:
for i in data["inputs"]:
if i["trigger"] == botengine.TRIGGER_DATA_REQUEST:
if len(logger.logs) > 0:
sys.stdout.write("logs: ")
for log in logger.logs:
sys.stdout.write(log + "; ")
if len(logger.tracebacks) > 0:
sys.stdout.write("tracebacks: ")
for tb in logger.tracebacks:
sys.stdout.write(tb + "; ")
sys.stdout.flush()
break
if "sqsQueue" in data:
send_sqs_message(
data.get("sqsQueue"),
json.dumps(logger.get_lambda_return(botengine, bot, allowed_output_size)),
data.get("clientContext"),
)
logger.info("<lambda_handler()")
return logger.get_lambda_return(botengine, bot, allowed_output_size)
def send_sqs_message(queue_name, msg_body, client_context):
"""
Method to deliver back to the server the logs and tracebacks during asynchronous parallel processed machine learning data request triggers
:param queue_name:
:param msg_body:
:param client_context:
:return:
"""
sqs = boto3.resource("sqs")
queue = sqs.get_queue_by_name(QueueName=queue_name)
queue.send_message(
MessageBody=msg_body,
MessageAttributes={
"ClientContext": {"StringValue": client_context, "DataType": "String"}
},
)
class LambdaLogger:
def __init__(self, log_level=LOG_LEVEL_DEFAULT):
# Tracebacks for crashes
# DEPRECATED: Captured from AWS Lambda
self.tracebacks = []
# Optional error message to return to the server
self.error_messages = []
# Logs
# DEPRECATED: Use log_events instead
self.logs = []
# Log events to return to the server
self.log_events = [] # [{"timestamp": time.time() * 1000, "message": "Log Me"}]
# Start Code - provided by the server in response to the Start API
self.start_code = 0
# Log level (info, debug, warn, error)
self.log_level = log_level
# Start time
self.start_time_ms = int(time.time() * 1000)
# Invoking service logging this message
self.service = "lambda"
# Current log time based on bot input
self.log_time = int(time.time() * 1000)
def log(self, level, message):
if level == "debug":
self.debug(message)
if level == "info":
self.info(message)
if level == "warn":
self.warn(message)
if level == "error":
self.error(message)
def debug(self, message):
if self.log_level in ["debug"]:
# Too granular
# self.logs.append("{}: [{}] {} {}".format(time.time(), "DEBUG", self.service, message))
self.log_events.append(
{
"timestamp": int(time.time() * 1000),
"message": "{} [{}] {} {}".format(self.log_time, "DEBUG", self.service, message),
}
)
def info(self, message):
if self.log_level in ["debug", "info"]:
# Too granular
# self.logs.append("{}: [{}] {} {}".format(time.time(), "INFO", self.service, message))
self.log_events.append(
{
"timestamp": int(time.time() * 1000),
"message": "{} [{}] {} {}".format(self.log_time, "INFO", self.service, message),
}
)
def warning(self, message):
self.warn(message)
def warn(self, message):
if self.log_level in ["debug", "info", "warn"]:
self.logs.append(
"{}: [{}] {} {}".format(time.time(), "WARN", self.service, message)
)
self.error_messages.append("[{}] {} {}".format("WARN", self.service, message))
self.log_events.append(
{
"timestamp": int(time.time() * 1000),
"message": "{} [{}] {} {}".format(self.log_time, "WARN", self.service, message),
}
)
def error(self, message):
self.logs.append(
"{}: {} [{}] {} {}".format(time.time(), self.log_time, "ERROR", self.service, message)
)
self.error_messages.append("[{}] {} {}".format("ERROR", self.service, message))
self.log_events.append(
{
"timestamp": int(time.time() * 1000),
"message": "{} [{}] {} {}".format(self.log_time, "ERROR", self.service, message),
}
)
def critical(self, message):
self.logs.append(
"{}: {} [{}] {} {}".format(time.time(), self.log_time, "CRITICAL", self.service, message)
)
self.error_messages.append("[{}] {} {}".format("CRITICAL", self.service, message))
self.log_events.append(
{
"timestamp": int(time.time() * 1000),
"message": "{} [{}] {} {}".format(self.log_time, "CRITICAL", self.service, message),
}
)
def exception(self, message):
self.logs.append(
"{}: {} [{}] {} {}".format(time.time(), self.log_time, "EXCEPTION", self.service, message)
)
self.error_messages.append("[{}] {} {}".format("EXCEPTION", self.service, message))
self.log_events.append(
{
"timestamp": int(time.time() * 1000),
"message": "{} [{}] {} {}".format(self.log_time, "EXCEPTION", self.service, message),
}
)
def get_lambda_return(
self, botengine=None, bot=None, allowed_output_size=256 * 1024
):
"""
Return the JSON structure to Lambda, limited to 256 KiB
by including logs and optional bot statistics
:param botengine: BotEngine instance
:param bot: Bot instance
:return: JSON dictionary of execution details
"""
response = {}
if botengine is None:
response["startTime"] = self.start_time_ms
response["endTime"] = int(time.time() * 1000)
response["logEvents"] = self.log_events
if self.error_messages:
response["errorMessage"] = "\n".join(self.error_messages)
if len(self.tracebacks):
response["tracebacks"] = self.tracebacks
return response
if botengine.get_bot_type() == botengine.BOT_TYPE_ORGANIZATION_RAG:
response["ragOutput"] = {}
if bot is not None:
if hasattr(bot, "get_document_property_updates"):
response["ragOutput"]["document"] = (
bot.get_document_property_updates(botengine)
)
if hasattr(bot, "get_documents_questions"):
response["ragOutput"]["questions"] = bot.get_documents_questions(
botengine
)
try:
self.debug(
"RAG documents executed: inputs ={}".format(
json.dumps(botengine.get_inputs())
)
)
self.debug(
"RAG documents updated : response={}".format(
json.dumps(response)
)
)
except Exception:
pass
response["startCode"] = self.start_code
response["startTime"] = self.start_time_ms
response["endTime"] = int(time.time() * 1000)
if self.error_messages:
response["errorMessage"] = "\n".join(self.error_messages)
if len(self.tracebacks):
response["tracebacks"] = self.tracebacks
# Prioritize logging outputs
priorities = [["EXCEPTION", "CRITICAL", "ERROR", "WARN"], ["INFO"], ["DEBUG"]]
response["logEvents"] = []
for priority in priorities:
for log in self.log_events:
size_available = allowed_output_size - len(
json.dumps(response).encode("utf-8")
)
priorities_available = {
level: f"[{level}]" in log["message"] for level in priority
}
if size_available and any(priorities_available.values()):
response["logEvents"].append(log)
# Limit response to 256 KiB
if len(json.dumps(response).encode("utf-8")) > allowed_output_size:
# Remove last log
if len(response["logEvents"]) > 0:
response["logEvents"].pop()
# Include additional bot server statistics for individual microservices if space allows
# and if the bot has the capability to provide them
if bot is not None and hasattr(bot, "get_intelligence_statistics"):
statistics = bot.get_intelligence_statistics(botengine)
if (
len(json.dumps(response).encode("utf-8"))
+ len(json.dumps(statistics).encode("utf-8"))
< allowed_output_size
):
response["microservices"] = statistics
return response