Skip to content

Commit 5177f32

Browse files
authored
Merge pull request #1 from kodemore/initial-implementation
Initial implementation
2 parents c87de1f + 7b00cf3 commit 5177f32

File tree

12 files changed

+1305
-1
lines changed

12 files changed

+1305
-1
lines changed

.github/workflows/main.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
lint_and_test:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
python-version: [3.8, 3.9]
17+
18+
steps:
19+
- uses: actions/checkout@v2
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v2
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install poetry
28+
poetry install
29+
- name: Linting
30+
run: |
31+
poetry run isort -c setup.cfg
32+
poetry run black --line-length=120 --target-version py38 chocs_middleware
33+
poetry run mypy chocs_middleware
34+
- name: Test with codecoverage
35+
run: |
36+
poetry run pytest --cov=./ --cov-report=xml
37+
- name: Upload coverage results
38+
uses: codecov/codecov-action@v1

.github/workflows/release.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- '*.*.*'
7+
8+
jobs:
9+
pypi:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: Get tag
14+
id: tag
15+
run: |
16+
echo ::set-output name=tag::${GITHUB_REF#refs/tags/}
17+
- name: Setup Python
18+
uses: actions/setup-python@v2
19+
with:
20+
python-version: '3.8'
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install poetry
25+
poetry install
26+
- name: Make build
27+
run: |
28+
poetry build -f sdist
29+
- name: Upload artifact
30+
uses: actions/upload-artifact@v1
31+
with:
32+
name: chocs-middleware-xray-${{ steps.tag.outputs.tag }}.tar.gz
33+
path: dist/chocs_middleware.xray-${{ steps.tag.outputs.tag }}.tar.gz
34+
- name: Publish release
35+
env:
36+
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
37+
run: |
38+
poetry publish
39+

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.idea
2+
.DS_Store
3+
__pycache__
4+
dist
5+
.pytest_cache

README.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,75 @@
11
# chocs-aws-xray
2-
AWS X-Ray middleware for chocs library
2+
AWS X-Ray middleware for chocs library.
3+
4+
## Installation
5+
through poetry:
6+
```shell
7+
poetry add chocs_middleware.xray
8+
```
9+
or through pip:
10+
```shell
11+
pip install chocs_middleware.xray
12+
```
13+
14+
## Usage
15+
16+
The following snippet is the simplest integration example.
17+
18+
> Please note x-ray won't work in WSGI mode, it has to be deployed as aws lambda in order to work.
19+
>
20+
```python
21+
from chocs import Application, HttpResponse, serve
22+
from chocs_middleware.xray import AwsXRayMiddleware
23+
24+
app = Application(AwsXRayMiddleware())
25+
26+
27+
@app.get("/hello")
28+
def say_hello(request):
29+
return HttpResponse("Hello")
30+
31+
serve(app)
32+
```
33+
34+
### Setting up custom error handler
35+
36+
AWS X-Ray middleware provides a way to setup a custom error handler which may become handy when you
37+
need to supplement your logs with additional information. Please consider the following example:
38+
39+
```python
40+
from chocs import Application, HttpResponse, HttpStatus
41+
from chocs_middleware.xray import AwsXRayMiddleware
42+
43+
def error_handler(request, error, segment):
44+
segment.add_exception(error)
45+
46+
return HttpResponse("NOT OK", HttpStatus.INTERNAL_SERVER_ERROR)
47+
48+
app = Application(AwsXRayMiddleware(error_handler=error_handler))
49+
50+
51+
@app.get("/hello")
52+
def say_hello(request):
53+
raise Exception("Not Today!")
54+
return HttpResponse("Hello")
55+
56+
```
57+
58+
> To learn more about error_handler interface please click [here.]("./chocs_middleware/xray/middleware.py:16")
59+
60+
### Accessing x-ray recorded from within your application layer
61+
```python
62+
from chocs import Application, HttpResponse
63+
from chocs_middleware.xray import AwsXRayMiddleware
64+
65+
app = Application(AwsXRayMiddleware())
66+
67+
@app.get("/hello")
68+
def say_hello(request):
69+
xray_recorder = request.attributes["aws_xray_recorder"] # Here is the instance of your recorder.
70+
71+
return HttpResponse("OK")
72+
73+
```
74+
75+
That's all.

chocs_middleware/xray/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .middleware import AwsXRayMiddleware

chocs_middleware/xray/middleware.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from aws_xray_sdk.core import AWSXRayRecorder, xray_recorder
2+
from aws_xray_sdk.core.lambda_launcher import check_in_lambda
3+
from aws_xray_sdk.core.models import http
4+
from aws_xray_sdk.core.models.segment import Segment
5+
from aws_xray_sdk.core.utils import stacktrace
6+
from aws_xray_sdk.ext.util import construct_xray_header, prepare_response_header
7+
from chocs import HttpRequest, HttpResponse, HttpStatus
8+
from chocs.errors import HttpError
9+
from chocs.middleware import Middleware, MiddlewareHandler
10+
from typing import Callable
11+
12+
__all__ = ["AwsXRayMiddleware"]
13+
14+
15+
ErrorHandler = Callable[[HttpRequest, Exception, Segment], HttpResponse]
16+
SegmentHandler = Callable[[HttpRequest, Segment], None]
17+
18+
19+
def default_error_handler(
20+
request: HttpRequest, error: Exception, segment: Segment
21+
) -> HttpResponse:
22+
23+
stack = stacktrace.get_stacktrace(limit=10)
24+
segment.add_exception(error, stack)
25+
26+
if isinstance(error, HttpError):
27+
response = HttpResponse(error.http_message, error.status_code)
28+
else:
29+
response = HttpResponse("Server Error", HttpStatus.INTERNAL_SERVER_ERROR)
30+
31+
return response
32+
33+
34+
class AwsXRayMiddleware(Middleware):
35+
def __init__(
36+
self,
37+
recorder: AWSXRayRecorder = None,
38+
error_handler: ErrorHandler = default_error_handler,
39+
segment_handler: SegmentHandler = None,
40+
):
41+
"""
42+
43+
:param recorder:
44+
:param error_handler: A callable that will be used if any error occurs during application execution
45+
:param segment_handler: A callable that will be used to provide extra information for x-ray segment
46+
"""
47+
48+
self._recorder = recorder if recorder is not None else xray_recorder
49+
self._error_handler = error_handler
50+
self._segment_handler = segment_handler
51+
52+
def __deepcopy__(self, memo):
53+
# Handle issue with deepcopying middleware when generating the MiddlewarePipeline for the application.
54+
# Since `xray_recorder` is globally instanciated, we can handle it separately to other attributes when
55+
# handling the deepcopy. This fix will work for cases where no custom recorder is used, however if one
56+
# does want to use a custom recorder, they may need to handle their own deepcopy pickling.
57+
58+
return AwsXRayMiddleware(
59+
self._recorder, self._error_handler, self._segment_handler
60+
)
61+
62+
def handle(self, request: HttpRequest, next: MiddlewareHandler) -> HttpResponse:
63+
# If we are not in lambda environment just ignore the middleware
64+
if not check_in_lambda():
65+
return next(request)
66+
67+
# There is some malfunction, so lets ignore it.
68+
if "__handler__" not in request.attributes:
69+
return next(request)
70+
71+
lambda_handler = request.attributes["__handler__"]
72+
73+
# Get the name of the handler function to use as the segment name.
74+
segment_name = lambda_handler.__name__
75+
76+
# Extract x-ray trace header from inbound request headers. Used by AWS internally to track
77+
# request/response cycle. When locally testing, the `aws_event` flag is not set on the request
78+
# attributes. In this case we fallback to the requests headers.
79+
xray_header = construct_xray_header(
80+
request.attributes.get("aws_event", {}).get("headers", request.headers)
81+
)
82+
83+
# Start subsegment for x-ray recording. We are in a lambda so we will always have a parent segment.
84+
segment = self._recorder.begin_subsegment(segment_name)
85+
86+
request.attributes["aws_xray_recorder"] = self._recorder
87+
88+
# Save x-ray trace header information to the current subsegment.
89+
segment.save_origin_trace_header(xray_header)
90+
91+
# Add request metadata to x-ray segment.
92+
segment.put_http_meta(http.METHOD, str(request.method))
93+
segment.put_http_meta(
94+
http.URL,
95+
str(request.path)
96+
+ ("?" + str(request.query_string) if request.query_string else ""),
97+
)
98+
99+
# Allow to append extra metadata to x-ray segment.
100+
if self._segment_handler:
101+
self._segment_handler(request, segment)
102+
103+
try:
104+
response = next(request)
105+
except Exception as error:
106+
response = self._error_handler(request, error, segment)
107+
108+
# Add the xray header from the inbound request to the response. Needed for AWS to keep track of the
109+
# request/response cycle internally in x-ray.
110+
response.headers[http.XRAY_HEADER] = prepare_response_header(
111+
xray_header, segment
112+
)
113+
segment.put_http_meta(http.STATUS, int(response.status_code))
114+
115+
self._recorder.end_subsegment()
116+
117+
return response

0 commit comments

Comments
 (0)