Skip to content

Commit a0fead2

Browse files
author
toshke
committed
Added body regex
1 parent 43ebcf2 commit a0fead2

File tree

4 files changed

+82
-28
lines changed

4 files changed

+82
-28
lines changed

README.md

+24-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ custom metrics, 0 otherwise, defaults to 1
3030
`CW_METRICS_NAMESPACE` - if CW custom metrics are being reported, this will determine
3131
their namespace, defaults to 'HttpCheck'
3232

33+
`BODY_REGEX_MATCH` - if CW custom metrics are being reported, this will enable `ResponseBodyRegexMatch`
34+
metric to be published as well, with value determined by success of matching response body against
35+
regular expression contained within this option
3336

3437

3538
## Outputs
@@ -48,6 +51,26 @@ is 2 minutes for http requests.
4851
`ResponseBody` - Optional, by default this won't be reported
4952

5053

54+
## Dependencies
55+
56+
Lambda function is having no external dependencies by design, so no additional packaging steps are required
57+
for deploying it, such as doing `pip install [libname]`
58+
59+
## CloudWatch Metrics
60+
61+
In order to get some metrics which you can alert on, `REPORT_AS_CW_METRICS` and `CW_METRICS_NAMESPACE` environment
62+
variables are used. Following metrics will be reported
63+
64+
- `Available` - 0 or 1, whether response was received in timely manner, indicating problems with network, DNS lookup or
65+
server timeout
66+
67+
- `TimeTaken` - Time taken to fetch response, reported in milliseconds
68+
69+
- `StatusCode` - HTTP Status code received from server
70+
71+
- `ResponseBodyRegexMatch` - **optional** this will report 1 or 0 if `BODY_REGEX_MATCH` option is specified. 1 is reported
72+
if response body matches regex provided, or 0 otherwise.
73+
5174
## Deployment
5275

5376
You can either deploy Lambda manually, or through [serverless](serverless.com) project.
@@ -76,7 +99,7 @@ sls invoke local -f httpcheck
7699
Optionally, for complicated example take a look at `test/ipify.json` file
77100

78101
```
79-
$ sls invoke local -f httpcheck -p test/ipifytimeout.json
102+
$ sls invoke local -f httpcheck -p test/ipify.json
80103
Failed to connect to https://api.ipify.org?format=json
81104
<urlopen error _ssl.c:732: The handshake operation timed out>
82105
Failed to publish metrics to CloudWatch:'TimeTaken'

handler.py

+47-26
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from time import perf_counter as pc
66
from urllib.parse import urlparse
77

8+
import re
9+
810

911
class Config:
1012
"""Lambda function runtime configuration"""
@@ -18,6 +20,7 @@ class Config:
1820
REPORT_AS_CW_METRICS = 'REPORT_AS_CW_METRICS'
1921
CW_METRICS_NAMESPACE = 'CW_METRICS_NAMESPACE'
2022
CW_METRICS_METRIC_NAME = 'CW_METRICS_METRIC_NAME'
23+
BODY_REGEX_MATCH = 'BODY_REGEX_MATCH'
2124

2225
def __init__(self, event):
2326
self.event = event
@@ -29,7 +32,8 @@ def __init__(self, event):
2932
self.REPORT_RESPONSE_BODY: '0',
3033
self.REPORT_AS_CW_METRICS: '1',
3134
self.CW_METRICS_NAMESPACE: 'HttpCheck',
32-
self.HEADERS: ''
35+
self.HEADERS: '',
36+
self.BODY_REGEX_MATCH: None
3337
}
3438

3539
def __get_property(self, property_name):
@@ -57,7 +61,7 @@ def payload(self):
5761
return payload
5862

5963
@property
60-
def timeoutms(self):
64+
def timeout(self):
6165
return self.__get_property(self.TIMEOUT)
6266

6367
@property
@@ -75,42 +79,47 @@ def headers(self):
7579
except:
7680
print(f"Could not decode headers: {headers}")
7781

82+
@property
83+
def bodyregexmatch(self):
84+
return self.__get_property(self.BODY_REGEX_MATCH)
85+
7886
@property
7987
def cwoptions(self):
8088
return {
8189
'enabled': self.__get_property(self.REPORT_AS_CW_METRICS),
82-
'namespace': self.__get_property(self.CW_METRICS_NAMESPACE)
90+
'namespace': self.__get_property(self.CW_METRICS_NAMESPACE),
8391
}
8492

8593

8694
class HttpCheck:
8795
"""Execution of HTTP(s) request"""
8896

89-
def __init__(self, endpoint, timeout=120000, method='GET', payload=None, headers={}):
90-
self.method = method
91-
self.endpoint = endpoint
92-
self.timeout = timeout
93-
self.payload = payload
94-
self.headers = headers
97+
def __init__(self, config):
98+
self.method = config.method
99+
self.endpoint = config.endpoint
100+
self.timeout = config.timeout
101+
self.payload = config.payload
102+
self.headers = config.headers
103+
self.bodyregexmatch = config.bodyregexmatch
95104

96105
def execute(self):
97106
url = urlparse(self.endpoint)
98107
location = url.netloc
99108
if url.scheme == 'http':
100109
request = http.client.HTTPConnection(location, timeout=int(self.timeout))
101-
110+
102111
if url.scheme == 'https':
103112
request = http.client.HTTPSConnection(location, timeout=int(self.timeout))
104-
113+
105114
if 'HTTP_DEBUG' in os.environ and os.environ['HTTP_DEBUG'] == '1':
106115
request.set_debuglevel(1)
107-
116+
108117
path = url.path
109118
if path == '':
110119
path = '/'
111120
if url.query is not None:
112121
path = path + "?" + url.query
113-
122+
114123
try:
115124
t0 = pc()
116125

@@ -122,14 +131,22 @@ def execute(self):
122131
# stop the stopwatch
123132
t1 = pc()
124133

125-
# return structure with data
126-
return {
134+
response_body = str(response_data.read().decode())
135+
result = {
127136
'Reason': response_data.reason,
128-
'ResponseBody': str(response_data.read().decode()),
137+
'ResponseBody': response_body,
129138
'StatusCode': response_data.status,
130139
'TimeTaken': int((t1 - t0) * 1000),
131140
'Available': '1'
132141
}
142+
143+
if self.bodyregexmatch is not None:
144+
regex = re.compile(self.bodyregexmatch)
145+
value = 1 if regex.match(response_body) else 0
146+
result['ResponseBodyRegexMatch'] = value
147+
148+
# return structure with data
149+
return result
133150
except Exception as e:
134151
print(f"Failed to connect to {self.endpoint}\n{e}")
135152
return {'Available': 0, 'Reason': str(e)}
@@ -171,6 +188,16 @@ def report(self, result):
171188
'Unit': 'None',
172189
'Value': int(result['StatusCode'])
173190
})
191+
if 'ResponseBodyRegexMatch' in result:
192+
metric_data.append({
193+
'MetricName': 'ResponseBodyRegexMatch',
194+
'Dimensions': [
195+
{'Name': 'Endpoint', 'Value': self.endpoint}
196+
],
197+
'Unit': 'None',
198+
'Value': int(result['ResponseBodyRegexMatch'])
199+
})
200+
174201
result = cloudwatch.put_metric_data(
175202
MetricData=metric_data,
176203
Namespace=self.options['namespace']
@@ -184,23 +211,17 @@ def http_check(event, context):
184211
"""Lambda function handler"""
185212

186213
config = Config(event)
187-
http_check = HttpCheck(
188-
config.endpoint,
189-
config.timeoutms,
190-
config.method,
191-
config.payload,
192-
config.headers
193-
)
214+
http_check = HttpCheck(config)
194215

195216
result = http_check.execute()
196217

218+
# report results
219+
ResultReporter(config, result).report(result)
220+
197221
# Remove body if not required
198222
if (config.reportbody != '1') and ('ResponseBody' in result):
199223
del result['ResponseBody']
200224

201-
# report results
202-
ResultReporter(config, result).report(result)
203-
204225
result_json = json.dumps(result, indent=4)
205226
# log results
206227
print(f"Result of checking {config.method} {config.endpoint}\n{result_json}")

test/ipify.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"TIMEOUT_MS":5,
55
"REPORT_RESPONSE_BODY":"1",
66
"REPORT_AS_CW_METRICS":"1",
7-
"CW_METRICS_NAMESPACE":"HttpCheckTestNamespace"
7+
"CW_METRICS_NAMESPACE":"HttpCheckTestNamespace1",
8+
"BODY_REGEX_MATCH":"\\{\"ip\":(.*)\\}"
89
}

test/ipifyRegexFail.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"ENDPOINT":"http://api.ipify.org",
3+
"METHOD":"GET",
4+
"TIMEOUT_MS":5,
5+
"REPORT_RESPONSE_BODY":"1",
6+
"REPORT_AS_CW_METRICS":"1",
7+
"CW_METRICS_NAMESPACE":"HttpCheckTestNamespace",
8+
"BODY_REGEX_MATCH":"\\{\"ip\":(.*)\\}"
9+
}

0 commit comments

Comments
 (0)