Skip to content

Commit 43ebcf2

Browse files
author
toshke
committed
Initial commit
Update
0 parents  commit 43ebcf2

File tree

6 files changed

+434
-0
lines changed

6 files changed

+434
-0
lines changed

.gitignore

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Distribution / packaging
2+
.Python
3+
env/
4+
build/
5+
develop-eggs/
6+
dist/
7+
downloads/
8+
eggs/
9+
.eggs/
10+
lib/
11+
lib64/
12+
parts/
13+
sdist/
14+
var/
15+
*.egg-info/
16+
.installed.cfg
17+
*.egg
18+
19+
# Serverless directories
20+
.serverless
21+
22+
__pycache__

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Lambda http check
2+
3+
Lambda function to check specific http endpoint, and report on it's availability.
4+
5+
6+
Optionally, it can record metrics to CloudWatch.
7+
8+
## Inputs
9+
10+
All inputs are either defined as environment variables or as part of event data. Event data
11+
will take priority over environment variables
12+
13+
`ENDPOINT` - url to be checked
14+
15+
`METHOD` - http method to use, defaults to `GET`
16+
17+
`PAYLOAD` - http payload, if `POST` or `PUT` used as method
18+
19+
`TIMEOUT` - timeout to use for http requests, defaults to 120s
20+
21+
`HEADERS` - list of headers to send to target server, defaults to empty list.
22+
Headers should be specified in
23+
24+
`REPORT_RESPONSE_BODY` - set to 1 if you wish to report on response body, 0
25+
otherwise, 0 otherwise, defaults to 0
26+
27+
`REPORT_AS_CW_METRICS` - set to 1 if you wish to store reported data as CW
28+
custom metrics, 0 otherwise, defaults to 1
29+
30+
`CW_METRICS_NAMESPACE` - if CW custom metrics are being reported, this will determine
31+
their namespace, defaults to 'HttpCheck'
32+
33+
34+
35+
## Outputs
36+
37+
By default, following properties will be rendered in output Json
38+
39+
`Reason` - Reason
40+
41+
`Available` - 0 or 1
42+
43+
`TimeTaken` - Time in ms it took to get response from remote server. Default timeout
44+
is 2 minutes for http requests.
45+
46+
`StatusCode` - Http Status Code
47+
48+
`ResponseBody` - Optional, by default this won't be reported
49+
50+
51+
## Deployment
52+
53+
You can either deploy Lambda manually, or through [serverless](serverless.com) project.
54+
If serverless is being chosen as method of deployments use command below, while
55+
making sure that you have setup proper access keys. For more information [read here](https://serverless.com/framework/docs/providers/aws/guide/workflow/)
56+
57+
Serverless framework version used during development
58+
is `1.23.0`, but it is very likely that later versions
59+
will function as well
60+
61+
```
62+
sls deploy
63+
```
64+
65+
If you are setting up your Lambda function by hand, make sure it has proper IAM
66+
permissions to push Cloud Watch metrics data, and to write to CloudWatch logs
67+
68+
## Testing
69+
70+
To test function locally with simple Google url (default), run following
71+
72+
```
73+
sls invoke local -f httpcheck
74+
```
75+
76+
Optionally, for complicated example take a look at `test/ipify.json` file
77+
78+
```
79+
$ sls invoke local -f httpcheck -p test/ipifytimeout.json
80+
Failed to connect to https://api.ipify.org?format=json
81+
<urlopen error _ssl.c:732: The handshake operation timed out>
82+
Failed to publish metrics to CloudWatch:'TimeTaken'
83+
Result of checking https://api.ipify.org?format=json
84+
{
85+
"Available": 0,
86+
"Reason": "<urlopen error _ssl.c:732: The handshake operation timed out>"
87+
}
88+
{
89+
"Available": 0,
90+
"Reason": "<urlopen error _ssl.c:732: The handshake operation timed out>"
91+
}
92+
```
93+
94+
## Debugging
95+
96+
If you wish to see debug output for http request, set `HTTP_DEBUG` environment
97+
variable to '1'. This can't be controlled through event payload.
98+
99+
## Schedule execution
100+
101+
Pull requests are welcome to serverless project to deploy CloudWatch rules in order
102+
to schedule execution of Http Checking Lambda function.

handler.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import json
2+
import os
3+
import http.client
4+
import boto3
5+
from time import perf_counter as pc
6+
from urllib.parse import urlparse
7+
8+
9+
class Config:
10+
"""Lambda function runtime configuration"""
11+
12+
ENDPOINT = 'ENDPOINT'
13+
METHOD = 'METHOD'
14+
PAYLOAD = 'PAYLOAD'
15+
TIMEOUT = 'TIMEOUT'
16+
HEADERS = 'HEADERS'
17+
REPORT_RESPONSE_BODY = 'REPORT_RESPONSE_BODY'
18+
REPORT_AS_CW_METRICS = 'REPORT_AS_CW_METRICS'
19+
CW_METRICS_NAMESPACE = 'CW_METRICS_NAMESPACE'
20+
CW_METRICS_METRIC_NAME = 'CW_METRICS_METRIC_NAME'
21+
22+
def __init__(self, event):
23+
self.event = event
24+
self.defaults = {
25+
self.ENDPOINT: 'https://google.com.au',
26+
self.METHOD: 'GET',
27+
self.PAYLOAD: None,
28+
self.TIMEOUT: 120,
29+
self.REPORT_RESPONSE_BODY: '0',
30+
self.REPORT_AS_CW_METRICS: '1',
31+
self.CW_METRICS_NAMESPACE: 'HttpCheck',
32+
self.HEADERS: ''
33+
}
34+
35+
def __get_property(self, property_name):
36+
if property_name in self.event:
37+
return self.event[property_name]
38+
if property_name in os.environ:
39+
return os.env[property_name]
40+
if property_name in self.defaults:
41+
return self.defaults[property_name]
42+
return None
43+
44+
@property
45+
def endpoint(self):
46+
return self.__get_property(self.ENDPOINT)
47+
48+
@property
49+
def method(self):
50+
return self.__get_property(self.METHOD)
51+
52+
@property
53+
def payload(self):
54+
payload = self.__get_property(self.PAYLOAD)
55+
if payload is not None:
56+
return payload.encode('utf-8')
57+
return payload
58+
59+
@property
60+
def timeoutms(self):
61+
return self.__get_property(self.TIMEOUT)
62+
63+
@property
64+
def reportbody(self):
65+
return self.__get_property(self.REPORT_RESPONSE_BODY)
66+
67+
@property
68+
def headers(self):
69+
headers = self.__get_property(self.HEADERS)
70+
if headers == '':
71+
return {}
72+
else:
73+
try:
74+
return dict(u.split("=") for u in headers.split(' '))
75+
except:
76+
print(f"Could not decode headers: {headers}")
77+
78+
@property
79+
def cwoptions(self):
80+
return {
81+
'enabled': self.__get_property(self.REPORT_AS_CW_METRICS),
82+
'namespace': self.__get_property(self.CW_METRICS_NAMESPACE)
83+
}
84+
85+
86+
class HttpCheck:
87+
"""Execution of HTTP(s) request"""
88+
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
95+
96+
def execute(self):
97+
url = urlparse(self.endpoint)
98+
location = url.netloc
99+
if url.scheme == 'http':
100+
request = http.client.HTTPConnection(location, timeout=int(self.timeout))
101+
102+
if url.scheme == 'https':
103+
request = http.client.HTTPSConnection(location, timeout=int(self.timeout))
104+
105+
if 'HTTP_DEBUG' in os.environ and os.environ['HTTP_DEBUG'] == '1':
106+
request.set_debuglevel(1)
107+
108+
path = url.path
109+
if path == '':
110+
path = '/'
111+
if url.query is not None:
112+
path = path + "?" + url.query
113+
114+
try:
115+
t0 = pc()
116+
117+
# perform request
118+
request.request(self.method, path, self.payload, self.headers)
119+
# read response
120+
response_data = request.getresponse()
121+
122+
# stop the stopwatch
123+
t1 = pc()
124+
125+
# return structure with data
126+
return {
127+
'Reason': response_data.reason,
128+
'ResponseBody': str(response_data.read().decode()),
129+
'StatusCode': response_data.status,
130+
'TimeTaken': int((t1 - t0) * 1000),
131+
'Available': '1'
132+
}
133+
except Exception as e:
134+
print(f"Failed to connect to {self.endpoint}\n{e}")
135+
return {'Available': 0, 'Reason': str(e)}
136+
137+
138+
class ResultReporter:
139+
"""Reporting results to CloudWatch"""
140+
141+
def __init__(self, config, context):
142+
self.options = config.cwoptions
143+
self.endpoint = config.endpoint
144+
145+
def report(self, result):
146+
if self.options['enabled'] == '1':
147+
try:
148+
cloudwatch = boto3.client('cloudwatch')
149+
metric_data = [{
150+
'MetricName': 'Available',
151+
'Dimensions': [
152+
{'Name': 'Endpoint', 'Value': self.endpoint}
153+
],
154+
'Unit': 'None',
155+
'Value': int(result['Available'])
156+
}]
157+
if result['Available'] == '1':
158+
metric_data.append({
159+
'MetricName': 'TimeTaken',
160+
'Dimensions': [
161+
{'Name': 'Endpoint', 'Value': self.endpoint}
162+
],
163+
'Unit': 'Milliseconds',
164+
'Value': int(result['TimeTaken'])
165+
})
166+
metric_data.append({
167+
'MetricName': 'StatusCode',
168+
'Dimensions': [
169+
{'Name': 'Endpoint', 'Value': self.endpoint}
170+
],
171+
'Unit': 'None',
172+
'Value': int(result['StatusCode'])
173+
})
174+
result = cloudwatch.put_metric_data(
175+
MetricData=metric_data,
176+
Namespace=self.options['namespace']
177+
)
178+
print(f"Sent data to CloudWatch requestId=:{result['ResponseMetadata']['RequestId']}")
179+
except Exception as e:
180+
print(f"Failed to publish metrics to CloudWatch:{e}")
181+
182+
183+
def http_check(event, context):
184+
"""Lambda function handler"""
185+
186+
config = Config(event)
187+
http_check = HttpCheck(
188+
config.endpoint,
189+
config.timeoutms,
190+
config.method,
191+
config.payload,
192+
config.headers
193+
)
194+
195+
result = http_check.execute()
196+
197+
# Remove body if not required
198+
if (config.reportbody != '1') and ('ResponseBody' in result):
199+
del result['ResponseBody']
200+
201+
# report results
202+
ResultReporter(config, result).report(result)
203+
204+
result_json = json.dumps(result, indent=4)
205+
# log results
206+
print(f"Result of checking {config.method} {config.endpoint}\n{result_json}")
207+
208+
# return to caller
209+
return result

0 commit comments

Comments
 (0)