Skip to content

Commit fd664b3

Browse files
authored
Merge pull request #40 from bakdata/feature/debug-logging-in-runtime
Use debug logging in runtime
2 parents 0737abc + 56d5e44 commit fd664b3

File tree

9 files changed

+89
-27
lines changed

9 files changed

+89
-27
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ See `awspack/` for an example.
231231
The `build.sh` script is used to run the docker container and copy sources to your machine.
232232
The `entrypoint.sh` script is used for installing packages inside the container.
233233

234+
### Debugging
235+
236+
In order to make the runtime log debugging messages, you can set the environment variable `LOGLEVEL` to `DEBUG`.
237+
234238
## Limitations
235239

236240
AWS Lambda is limited to running with 3GB RAM and must finish within 15 minutes.

runtime/src/bootstrap.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ tryCatch({
33
function_name <- initializeRuntime()
44
while (TRUE) {
55
handle_request(function_name)
6+
logReset()
67
rm(list=ls())
78
source('/opt/runtime.R')
89
function_name <- initializeRuntime()

runtime/src/runtime.R

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ error_to_payload <- function(error) {
77
}
88

99
post_error <- function(error, url) {
10-
logerror(error)
10+
logerror(error, logger = 'runtime')
1111
res <- POST(url,
1212
add_headers("Lambda-Runtime-Function-Error-Type" = "Unhandled"),
1313
body = error_to_payload(error),
1414
encode = "json")
15-
loginfo("Posted result:\n%s", to_str(res))
15+
logdebug("Posted result:\n%s", to_str(res), logger = 'runtime')
1616
}
1717

1818
get_source_file_name <- function(file_base_name) {
@@ -28,22 +28,33 @@ get_source_file_name <- function(file_base_name) {
2828

2929
invoke_lambda <- function(EVENT_DATA, function_name) {
3030
params <- fromJSON(EVENT_DATA)
31-
loginfo("Invoking function '%s' with parameters:\n%s", function_name, to_str(params))
31+
logdebug("Invoking function '%s' with parameters:\n%s", function_name, to_str(params), logger = 'runtime')
3232
result <- do.call(function_name, params)
33-
loginfo("Function returned:\n%s", to_str(result))
33+
logdebug("Function returned:\n%s", to_str(result), logger = 'runtime')
3434
return(result)
3535
}
3636

37+
initializeLogging <- function() {
38+
library(logging)
39+
40+
basicConfig()
41+
addHandler(writeToConsole, logger='runtime')
42+
log_level <- Sys.getenv('LOGLEVEL', unset = NA)
43+
if (!is.na(log_level)) {
44+
setLevel(log_level, 'runtime')
45+
}
46+
}
47+
3748
initializeRuntime <- function() {
3849
library(httr)
3950
library(jsonlite)
40-
library(logging)
4151

52+
initializeLogging()
4253
HANDLER <- Sys.getenv("_HANDLER")
4354
HANDLER_split <- strsplit(HANDLER, ".", fixed = TRUE)[[1]]
4455
file_base_name <- HANDLER_split[1]
4556
file_name <- get_source_file_name(file_base_name)
46-
loginfo("Sourcing '%s'", file_name)
57+
logdebug("Sourcing '%s'", file_name, logger = 'runtime')
4758
source(file_name)
4859
function_name <- HANDLER_split[2]
4960
if (!exists(function_name, mode = "function")) {
@@ -69,7 +80,7 @@ throwRuntimeError <- function(error, REQUEST_ID) {
6980
postResult <- function(result, REQUEST_ID) {
7081
url <- paste0(API_ENDPOINT, "invocation/", REQUEST_ID, "/response")
7182
res <- POST(url, body = toJSON(result, auto_unbox = TRUE), encode = "raw", content_type_json())
72-
loginfo("Posted result:\n%s", to_str(res))
83+
logdebug("Posted result:\n%s", to_str(res), logger = 'runtime')
7384
}
7485

7586
handle_request <- function(function_name) {

test-template.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Parameters:
66
Globals:
77
Function:
88
Runtime: provided
9-
Timeout: 300
9+
Timeout: 900
1010
MemorySize: 3008
1111
Layers:
1212
- !Ref RuntimeLayer
@@ -53,6 +53,15 @@ Resources:
5353
Handler: script.handler_with_variable_arguments
5454
CodeUri: tests/R/
5555
FunctionName: !Sub VariableArgumentsFunction-${Version}
56+
LoggingFunction:
57+
Type: 'AWS::Serverless::Function'
58+
Properties:
59+
Handler: script.handler_with_debug_logging
60+
CodeUri: tests/R/
61+
Environment:
62+
Variables:
63+
LOGLEVEL: DEBUG
64+
FunctionName: !Sub LoggingFunction-${Version}
5665
MatrixFunction:
5766
Type: 'AWS::Serverless::Function'
5867
Properties:

tests/R/script.R

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ handler_with_variable_arguments <- function(...) {
1010
return(1)
1111
}
1212

13-
handler_as_variable <- "foo"
13+
handler_as_variable <- "foo"
14+
15+
handler_with_debug_logging <- function(x) {
16+
return(1)
17+
}

tests/sam.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def __init__(self,
7272

7373
def get_client(self):
7474
config = botocore.client.Config(signature_version=botocore.UNSIGNED,
75-
read_timeout=300,
75+
read_timeout=900,
7676
retries={'max_attempts': 0},
7777
)
7878
return boto3.client('lambda',

tests/test_aws.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def test_s3_get_object(self):
2626
response = lambda_client.invoke(FunctionName=get_function_name("AWSFunction"))
2727
raw_payload = response['Payload'].read().decode('utf-8')
2828
result = json.loads(raw_payload)
29-
self.assertEqual(len(result), 1)
30-
self.assertDictEqual(result[0], {
29+
self.assertEqual(1, len(result))
30+
self.assertDictEqual({
3131
"DRG.Definition": "039 - EXTRACRANIAL PROCEDURES W/O CC/MCC",
3232
"Provider.Id": "10001",
3333
"Provider.Name": "SOUTHEAST ALABAMA MEDICAL CENTER",
@@ -40,7 +40,7 @@ def test_s3_get_object(self):
4040
"Average.Covered.Charges": "$32963.07",
4141
"Average.Total.Payments": "$5777.24",
4242
"Average.Medicare.Payments": "$4763.73"
43-
})
43+
}, result[0])
4444

4545
@classmethod
4646
def tearDownClass(cls):

tests/test_matrix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_matrix(self):
2525
response = lambda_client.invoke(FunctionName=get_function_name("MatrixFunction"))
2626
raw_payload = response['Payload'].read().decode('utf-8')
2727
result = json.loads(raw_payload)
28-
self.assertEqual(len(result), 3)
28+
self.assertEqual(3, len(result))
2929
self.assertIn(4, result)
3030
self.assertIn(5, result)
3131
self.assertIn(6, result)

tests/test_runtime.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import base64
12
import json
2-
import os
3+
import re
34
import unittest
45

56
import boto3
@@ -28,7 +29,7 @@ def test_script(self):
2829
)
2930
raw_payload = response['Payload'].read().decode('utf-8')
3031
result = json.loads(raw_payload)
31-
self.assertEqual(result, 2)
32+
self.assertEqual(2, result)
3233

3334
def test_lowercase_extension(self):
3435
lambda_client = self.get_client()
@@ -37,7 +38,7 @@ def test_lowercase_extension(self):
3738
)
3839
raw_payload = response['Payload'].read().decode('utf-8')
3940
result = json.loads(raw_payload)
40-
self.assertEqual(result, 2)
41+
self.assertEqual(2, result)
4142

4243
def test_multiple_arguments(self):
4344
lambda_client = self.get_client()
@@ -47,7 +48,39 @@ def test_multiple_arguments(self):
4748
)
4849
raw_payload = response['Payload'].read().decode('utf-8')
4950
result = json.loads(raw_payload)
50-
self.assertDictEqual(result, payload)
51+
self.assertDictEqual(payload, result)
52+
53+
@unittest.skipIf(is_local(), 'Lambda local does not support log retrieval')
54+
def test_debug_logging(self):
55+
lambda_client = self.get_client()
56+
response = lambda_client.invoke(FunctionName=get_function_name("LoggingFunction"),
57+
LogType='Tail',
58+
Payload=json.dumps({'x': 1}),
59+
)
60+
raw_payload = response['Payload'].read().decode('utf-8')
61+
result = json.loads(raw_payload)
62+
self.assertEqual(1, result)
63+
log = base64.b64decode(response['LogResult']).decode('utf-8')
64+
self.assertIn("runtime:Sourcing 'script.R'", log)
65+
self.assertIn("runtime:Invoking function 'handler_with_debug_logging' with parameters:\n$x\n[1] 1", log)
66+
self.assertIn("runtime:Function returned:\n[1] 1", log)
67+
self.assertIn("runtime:Posted result:\n", log)
68+
69+
@unittest.skipIf(is_local(), 'Lambda local does not support log retrieval')
70+
def test_no_debug_logging(self):
71+
lambda_client = self.get_client()
72+
response = lambda_client.invoke(FunctionName=get_function_name("ExampleFunction"),
73+
LogType='Tail',
74+
Payload=json.dumps({'x': 1}),
75+
)
76+
raw_payload = response['Payload'].read().decode('utf-8')
77+
result = json.loads(raw_payload)
78+
self.assertEqual(2, result)
79+
log = base64.b64decode(response['LogResult']).decode('utf-8')
80+
self.assertNotIn("Sourcing ", log)
81+
self.assertNotIn("Invoking function ", log)
82+
self.assertNotIn("Function returned:", log)
83+
self.assertNotIn("Posted result:", log)
5184

5285
@unittest.skipIf(is_local(), 'Lambda local does not pass errors properly')
5386
def test_missing_source_file(self):
@@ -58,7 +91,7 @@ def test_missing_source_file(self):
5891
raw_payload = response['Payload'].read().decode('utf-8')
5992
json_payload = json.loads(raw_payload)
6093
self.assertIn('Source file does not exist: missing.[R|r]', json_payload['errorMessage'])
61-
self.assertEqual(json_payload['errorType'], 'simpleError')
94+
self.assertEqual('simpleError', json_payload['errorType'])
6295

6396
@unittest.skipIf(is_local(), 'Lambda local does not pass errors properly')
6497
def test_missing_function(self):
@@ -69,7 +102,7 @@ def test_missing_function(self):
69102
raw_payload = response['Payload'].read().decode('utf-8')
70103
json_payload = json.loads(raw_payload)
71104
self.assertIn('Function "handler_missing" does not exist', json_payload['errorMessage'])
72-
self.assertEqual(json_payload['errorType'], 'simpleError')
105+
self.assertEqual('simpleError', json_payload['errorType'])
73106

74107
@unittest.skipIf(is_local(), 'Lambda local does not pass errors properly')
75108
def test_function_as_variable(self):
@@ -80,7 +113,7 @@ def test_function_as_variable(self):
80113
raw_payload = response['Payload'].read().decode('utf-8')
81114
json_payload = json.loads(raw_payload)
82115
self.assertIn('Function "handler_as_variable" does not exist', json_payload['errorMessage'])
83-
self.assertEqual(json_payload['errorType'], 'simpleError')
116+
self.assertEqual('simpleError', json_payload['errorType'])
84117

85118
@unittest.skipIf(is_local(), 'Lambda local does not pass errors properly')
86119
def test_missing_argument(self):
@@ -89,7 +122,7 @@ def test_missing_argument(self):
89122
raw_payload = response['Payload'].read().decode('utf-8')
90123
json_payload = json.loads(raw_payload)
91124
self.assertIn('argument "x" is missing, with no default', json_payload['errorMessage'])
92-
self.assertEqual(json_payload['errorType'], 'simpleError')
125+
self.assertEqual('simpleError', json_payload['errorType'])
93126

94127
@unittest.skipIf(is_local(), 'Lambda local does not pass errors properly')
95128
def test_unused_argument(self):
@@ -100,18 +133,18 @@ def test_unused_argument(self):
100133
raw_payload = response['Payload'].read().decode('utf-8')
101134
json_payload = json.loads(raw_payload)
102135
self.assertIn('unused argument (y = 1)', json_payload['errorMessage'])
103-
self.assertEqual(json_payload['errorType'], 'simpleError')
136+
self.assertEqual('simpleError', json_payload['errorType'])
104137

105-
@unittest.skip('Lambda local does not pass errors properly')
138+
@unittest.skipIf(is_local(), 'Fails locally with "argument list too long"')
106139
def test_long_argument(self):
107140
lambda_client = self.get_client()
108141
payload = {x: x for x in range(0, 100000)}
109142
response = lambda_client.invoke(FunctionName=get_function_name("VariableArgumentsFunction"),
110143
Payload=json.dumps(payload),
111144
)
112145
raw_payload = response['Payload'].read().decode('utf-8')
113-
json_payload = json.loads(raw_payload)
114-
self.assertEqual(json_payload['errorType'], 'Runtime.ExitError')
146+
result = json.loads(raw_payload)
147+
self.assertEqual(1, result)
115148

116149
@unittest.skipIf(is_local(), 'Lambda local does not pass errors properly')
117150
def test_missing_library(self):
@@ -123,7 +156,7 @@ def test_missing_library(self):
123156
json_payload = json.loads(raw_payload)
124157
self.assertIn('there is no package called ‘Matrix’', json_payload['errorMessage'])
125158
error_type = 'packageNotFoundError' if get_version() == '3_6_0' else 'simpleError'
126-
self.assertEqual(json_payload['errorType'], error_type)
159+
self.assertEqual(error_type, json_payload['errorType'])
127160

128161
@classmethod
129162
def tearDownClass(cls):

0 commit comments

Comments
 (0)