Skip to content

Commit 33d4f83

Browse files
committed
Initial work on tracing middleware
1 parent 6df8a63 commit 33d4f83

File tree

3 files changed

+83
-8
lines changed

3 files changed

+83
-8
lines changed

graphql/execution/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@ class ExecutionResult(object):
128128

129129
__slots__ = 'data', 'errors', 'invalid'
130130

131-
def __init__(self, data=None, errors=None, invalid=False):
131+
def __init__(self, data=None, errors=None, extensions=None, invalid=False):
132132
self.data = data
133133
self.errors = errors
134+
self.extensions = extensions or dict()
134135

135136
if invalid:
136137
assert data is None

graphql/execution/executor.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
get_operation_root_type, SubscriberExecutionContext)
2020
from .executors.sync import SyncExecutor
2121
from .middleware import MiddlewareManager
22+
from .tracing import TracingMiddleware
2223

2324
logger = logging.getLogger(__name__)
2425

@@ -30,12 +31,23 @@ def subscribe(*args, **kwargs):
3031

3132
def execute(schema, document_ast, root_value=None, context_value=None,
3233
variable_values=None, operation_name=None, executor=None,
33-
return_promise=False, middleware=None, allow_subscriptions=False):
34+
return_promise=False, middleware=None, allow_subscriptions=False,
35+
tracing=True):
3436
assert schema, 'Must provide schema'
3537
assert isinstance(schema, GraphQLSchema), (
3638
'Schema must be an instance of GraphQLSchema. Also ensure that there are ' +
3739
'not multiple versions of GraphQL installed in your node_modules directory.'
3840
)
41+
42+
if tracing:
43+
tracing_middleware = TracingMiddleware()
44+
tracing_middleware.start()
45+
46+
if not isinstance(middleware, MiddlewareManager):
47+
middleware = MiddlewareManager(tracing_middleware)
48+
else:
49+
middleware.middlewares.insert(0, tracing_middleware)
50+
3951
if middleware:
4052
if not isinstance(middleware, MiddlewareManager):
4153
middleware = MiddlewareManager(*middleware)
@@ -71,12 +83,14 @@ def on_resolve(data):
7183
if isinstance(data, Observable):
7284
return data
7385

86+
extensions = dict(tracing=tracing_middleware.tracing_dict)
87+
7488
if not context.errors:
75-
return ExecutionResult(data=data)
76-
return ExecutionResult(data=data, errors=context.errors)
89+
return ExecutionResult(data=data, extensions=extensions)
90+
91+
return ExecutionResult(data=data, extensions=extensions, errors=context.errors)
7792

78-
promise = Promise.resolve(None).then(
79-
executor).catch(on_rejected).then(on_resolve)
93+
promise = Promise.resolve(None).then(executor).catch(on_rejected).then(on_resolve)
8094

8195
if not return_promise:
8296
context.executor.wait_until_finished()
@@ -326,8 +340,7 @@ def complete_value_catching_error(exe_context, return_type, field_asts, info, re
326340
# Otherwise, error protection is applied, logging the error and
327341
# resolving a null value for this field if one is encountered.
328342
try:
329-
completed = complete_value(
330-
exe_context, return_type, field_asts, info, result)
343+
completed = complete_value(exe_context, return_type, field_asts, info, result)
331344
if is_thenable(completed):
332345
def handle_error(error):
333346
traceback = completed._traceback

graphql/execution/tracing.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import time
2+
import datetime
3+
4+
class TracingMiddleware(object):
5+
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
6+
7+
def __init__(self):
8+
self.resolver_stats = list()
9+
self.start_time = None
10+
self.end_time = None
11+
12+
def start(self):
13+
self.start_time = time.time()
14+
15+
def end(self):
16+
self.end_time = time.time()
17+
18+
@property
19+
def start_time_str(self):
20+
return time.strftime(self.DATETIME_FORMAT, time.gmtime(self.start_time))
21+
22+
@property
23+
def end_time_str(self):
24+
return time.strftime(self.DATETIME_FORMAT, time.gmtime(self.end_time))
25+
26+
@property
27+
def duration(self):
28+
if not self.end_time:
29+
raise ValueError("Tracing has not ended yet!")
30+
31+
return (self.end_time - self.start_time) * 1000
32+
33+
@property
34+
def tracing_dict(self):
35+
return dict(
36+
version=1,
37+
startTime=self.start_time,
38+
endTime=self.end_time,
39+
duration=self.duration,
40+
execution=dict(
41+
resolvers=self.resolver_stats
42+
)
43+
)
44+
45+
def resolve(self, _next, root, info, *args, **kwargs):
46+
start = time.time()
47+
try:
48+
return _next(root, info, *args, **kwargs)
49+
finally:
50+
end = time.time()
51+
elapsed_ms = (end - start) * 1000
52+
53+
stat = {
54+
# "path": [ <>, ...],
55+
"parentType": str(info.parent_type),
56+
"fieldName": info.field_name,
57+
"returnType": str(info.return_type),
58+
"startOffset": (time.time() - self.start_time) * 1000,
59+
"duration": elapsed_ms,
60+
}
61+
self.resolver_stats.append(stat)

0 commit comments

Comments
 (0)