Skip to content

Commit 18051d9

Browse files
committed
add a single middleware for apps to include that orchestrates things the right way
1 parent 44e6171 commit 18051d9

File tree

6 files changed

+113
-82
lines changed

6 files changed

+113
-82
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
A single middleware to provide a unified observability layer, ensuring that context,
3+
profiling, and SQL metrics are captured in the correct order.
4+
"""
5+
6+
from .profiling.profile_request import _ProfileRequestMiddleware, _SQLProfilingMiddleware
7+
from .request_context import _TraceContextMiddleware
8+
9+
10+
class ObservabilityMiddleware:
11+
"""
12+
A single entry point for observability middleware.
13+
14+
This middleware composes the trace context, request profiling, and SQL
15+
profiling middleware in the correct order. Instead of listing all three
16+
in your settings, you can now just add this one.
17+
"""
18+
19+
def __init__(self, get_response):
20+
# Chain the middleware in the desired order. The request will flow
21+
# from _TraceContextMiddleware -> _ProfileRequestMiddleware -> _SQLProfilingMiddleware.
22+
handler = _SQLProfilingMiddleware(get_response)
23+
handler = _ProfileRequestMiddleware(handler)
24+
self.handler = _TraceContextMiddleware(handler)
25+
26+
def __call__(self, request):
27+
return self.handler(request)

ansible_base/lib/middleware/profiling/README.md

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
# Request Profiling
1+
# Request Profiling and Observability
22

3-
The `ProfileRequestMiddleware` and `DABProfiler` class provide a way to profile requests and other code in your Django application. This functionality is a generalization of the profiling tools found in AWX and can be used by any `django-ansible-base` consumer.
3+
The `ObservabilityMiddleware` provides a simple way to gain performance and debugging insights into your Django application. It acts as a single entry point for several underlying middleware components, ensuring they are always used in the correct order.
44

5-
## `ProfileRequestMiddleware`
5+
## `ObservabilityMiddleware`
66

7-
This middleware provides performance insights for API requests. To use it, add it to your `MIDDLEWARE` list in your Django settings. For the most accurate and reliable timing, it is recommended to add this middleware to the top of the `MIDDLEWARE` list.
7+
This single middleware bundles tracing, request profiling, and SQL query analysis. To use it, add it to the top of your `MIDDLEWARE` list in your Django settings.
88

99
```python
1010
# settings.py
1111
MIDDLEWARE = [
12-
'ansible_base.lib.middleware.profiling.profile_request.ProfileRequestMiddleware',
12+
'ansible_base.lib.middleware.observability.ObservabilityMiddleware',
1313
...
1414
]
1515
```
1616

1717
The middleware always adds the following headers to the response:
1818

19+
* `X-Request-ID`: A unique identifier for the request. If the incoming request includes an `X-Request-ID` header, that value will be used; otherwise, a new UUID will be generated.
1920
* `X-API-Time`: The total time taken to process the request, in seconds.
20-
* `X-API-Node`: The cluster host ID of the node that served the request. This header is only added if it is not already present in the response.
21+
* `X-API-Node`: The cluster host ID of the node that served the request.
2122

2223
### cProfile Support
2324

24-
When the `ANSIBLE_BASE_CPROFILE_REQUESTS` setting is enabled, the middleware will also perform a cProfile analysis for each request. The resulting `.prof` file is saved to a temporary directory on the node that served the request, and its path is returned in the `X-API-CProfile-File` response header.
25+
When the `ANSIBLE_BASE_CPROFILE_REQUESTS` setting is enabled, the middleware will also perform a cProfile analysis for each request. The resulting `.prof` file is saved to a temporary directory on the node that served the request, and its path is returned in the `X-API-CProfile-File` response header. The filename will include the request's `X-Request-ID`.
2526

2627
To enable cProfile support, set the following in your Django settings:
2728

@@ -30,32 +31,16 @@ To enable cProfile support, set the following in your Django settings:
3031
ANSIBLE_BASE_CPROFILE_REQUESTS = True
3132
```
3233

33-
## `SQLProfilingMiddleware`
34+
### SQL Profiling Support
3435

35-
This middleware provides insights into the database queries executed during a request. When enabled, it adds the following headers to the response:
36+
When the `ANSIBLE_BASE_SQL_PROFILING` setting is enabled, the middleware provides insights into the database queries executed during a request. It adds the following headers to the response:
3637

3738
* `X-API-Query-Count`: The total number of database queries executed during the request.
3839
* `X-API-Query-Time`: The total time spent on database queries, in seconds.
3940

4041
It also injects contextual information as a comment into each SQL query, which is invaluable for debugging and tracing. For example:
4142
`/* trace_id=b71696ed-c483-408d-9740-2e7935b4f2d9, route=api/v2/users/{pk}/, origin=request */ SELECT ...`
4243

43-
To use it, add both the `TraceContextMiddleware` and the `SQLProfilingMiddleware` to your `MIDDLEWARE` list in your Django settings. The `TraceContextMiddleware` should come before the `SQLProfilingMiddleware`.
44-
45-
```python
46-
# settings.py
47-
MIDDLEWARE = [
48-
...
49-
'ansible_base.lib.middleware.request_context.TraceContextMiddleware',
50-
'ansible_base.lib.middleware.profiling.profile_request.SQLProfilingMiddleware',
51-
...
52-
]
53-
```
54-
55-
### Enabling SQL Profiling
56-
57-
The middleware is controlled by the `ANSIBLE_BASE_SQL_PROFILING` setting.
58-
5944
To enable SQL profiling, set the following in your Django settings:
6045

6146
```python
@@ -65,7 +50,7 @@ ANSIBLE_BASE_SQL_PROFILING = True
6550

6651
## `DABProfiler`
6752

68-
The core profiling logic is encapsulated in the `DABProfiler` class. This class can be imported and used directly for profiling non-HTTP contexts, such as background tasks or gRPC services.
53+
For profiling non-HTTP contexts, such as background tasks or gRPC services, the `DABProfiler` class can be used directly.
6954

7055
The profiler's cProfile functionality is controlled by the `ANSIBLE_BASE_CPROFILE_REQUESTS` setting.
7156

@@ -119,3 +104,4 @@ import pstats
119104
p = pstats.Stats('/path/to/your/profile.prof')
120105
p.sort_stats('cumulative').print_stats(10)
121106
```
107+

ansible_base/lib/middleware/profiling/profile_request.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ def stop(self, profile_id: Optional[Union[str, uuid.UUID]] = None):
5151
return elapsed, cprofile_filename
5252

5353

54-
class ProfileRequestMiddleware(threading.local):
54+
class _ProfileRequestMiddleware(threading.local):
5555
def __init__(self, get_response=None):
5656
self.get_response = get_response
5757
self.profiler = DABProfiler()
5858

5959
def __call__(self, request):
6060
# Logic before the view (formerly process_request)
6161
self.profiler.start()
62-
request_id = request.headers.get('X-Request-ID')
62+
request_id = trace_id_var.get()
6363

6464
# Call the next middleware or the view
6565
response = self.get_response(request)
@@ -111,7 +111,7 @@ def __call__(self, execute, sql, params, many, context):
111111
self.query_time += time.time() - start_time
112112

113113

114-
class SQLProfilingMiddleware:
114+
class _SQLProfilingMiddleware:
115115
def __init__(self, get_response):
116116
self.get_response = get_response
117117

@@ -123,7 +123,7 @@ def __call__(self, request):
123123
if trace_id_var.get() is None:
124124
logger.warning(
125125
"ANSIBLE_BASE_SQL_PROFILING is enabled, but the trace context is not set. "
126-
"Please ensure that TraceContextMiddleware is included in your MIDDLEWARE settings before this middleware."
126+
"Please use the ObservabilityMiddleware instead of including profiling middleware individually."
127127
)
128128

129129
metrics = SQLQueryMetrics()

ansible_base/lib/middleware/request_context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
from ansible_base.lib.logging.context import origin_var, route_var, trace_id_var
44

55

6-
class TraceContextMiddleware:
6+
class _TraceContextMiddleware:
77
def __init__(self, get_response):
88
self.get_response = get_response
99

1010
def __call__(self, request):
1111
# Set the context for the request and store the tokens
1212
origin_token = origin_var.set('request')
13-
trace_id = request.headers.get('X-Request-ID', str(uuid.uuid4()))
13+
# .get is case-insensitive, but we'll use lowercase for consistency
14+
trace_id = request.headers.get('x-request-id', str(uuid.uuid4()))
1415
trace_id_token = trace_id_var.set(trace_id)
1516

1617
route_token = None
@@ -19,6 +20,7 @@ def __call__(self, request):
1920

2021
try:
2122
response = self.get_response(request)
23+
response['X-Request-ID'] = trace_id
2224
finally:
2325
# Reset the context variables to their previous state
2426
origin_var.reset(origin_token)

0 commit comments

Comments
 (0)