@@ -60,6 +60,7 @@ def add(x, y):
60
60
"""
61
61
62
62
import logging
63
+ from timeit import default_timer
63
64
from typing import Collection , Iterable
64
65
65
66
from celery import signals # pylint: disable=no-name-in-module
@@ -69,6 +70,7 @@ def add(x, y):
69
70
from opentelemetry .instrumentation .celery .package import _instruments
70
71
from opentelemetry .instrumentation .celery .version import __version__
71
72
from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
73
+ from opentelemetry .metrics import get_meter
72
74
from opentelemetry .propagate import extract , inject
73
75
from opentelemetry .propagators .textmap import Getter
74
76
from opentelemetry .semconv .trace import SpanAttributes
@@ -104,6 +106,11 @@ def keys(self, carrier):
104
106
105
107
106
108
class CeleryInstrumentor (BaseInstrumentor ):
109
+ def __init__ (self ):
110
+ super ().__init__ ()
111
+ self .metrics = None
112
+ self .task_id_to_start_time = {}
113
+
107
114
def instrumentation_dependencies (self ) -> Collection [str ]:
108
115
return _instruments
109
116
@@ -113,6 +120,11 @@ def _instrument(self, **kwargs):
113
120
# pylint: disable=attribute-defined-outside-init
114
121
self ._tracer = trace .get_tracer (__name__ , __version__ , tracer_provider )
115
122
123
+ meter_provider = kwargs .get ("meter_provider" )
124
+ meter = get_meter (__name__ , __version__ , meter_provider )
125
+
126
+ self .create_celery_metrics (meter )
127
+
116
128
signals .task_prerun .connect (self ._trace_prerun , weak = False )
117
129
signals .task_postrun .connect (self ._trace_postrun , weak = False )
118
130
signals .before_task_publish .connect (
@@ -139,6 +151,7 @@ def _trace_prerun(self, *args, **kwargs):
139
151
if task is None or task_id is None :
140
152
return
141
153
154
+ self .update_task_duration_time (task_id )
142
155
request = task .request
143
156
tracectx = extract (request , getter = celery_getter ) or None
144
157
@@ -153,8 +166,7 @@ def _trace_prerun(self, *args, **kwargs):
153
166
activation .__enter__ () # pylint: disable=E1101
154
167
utils .attach_span (task , task_id , (span , activation ))
155
168
156
- @staticmethod
157
- def _trace_postrun (* args , ** kwargs ):
169
+ def _trace_postrun (self , * args , ** kwargs ):
158
170
task = utils .retrieve_task (kwargs )
159
171
task_id = utils .retrieve_task_id (kwargs )
160
172
@@ -178,6 +190,9 @@ def _trace_postrun(*args, **kwargs):
178
190
179
191
activation .__exit__ (None , None , None )
180
192
utils .detach_span (task , task_id )
193
+ self .update_task_duration_time (task_id )
194
+ labels = {"task" : task .name , "worker" : task .request .hostname }
195
+ self ._record_histograms (task_id , labels )
181
196
182
197
def _trace_before_publish (self , * args , ** kwargs ):
183
198
task = utils .retrieve_task_from_sender (kwargs )
@@ -277,3 +292,30 @@ def _trace_retry(*args, **kwargs):
277
292
# Use `str(reason)` instead of `reason.message` in case we get
278
293
# something that isn't an `Exception`
279
294
span .set_attribute (_TASK_RETRY_REASON_KEY , str (reason ))
295
+
296
+ def update_task_duration_time (self , task_id ):
297
+ cur_time = default_timer ()
298
+ task_duration_time_until_now = (
299
+ cur_time - self .task_id_to_start_time [task_id ]
300
+ if task_id in self .task_id_to_start_time
301
+ else cur_time
302
+ )
303
+ self .task_id_to_start_time [task_id ] = task_duration_time_until_now
304
+
305
+ def _record_histograms (self , task_id , metric_attributes ):
306
+ if task_id is None :
307
+ return
308
+
309
+ self .metrics ["flower.task.runtime.seconds" ].record (
310
+ self .task_id_to_start_time .get (task_id ),
311
+ attributes = metric_attributes ,
312
+ )
313
+
314
+ def create_celery_metrics (self , meter ) -> None :
315
+ self .metrics = {
316
+ "flower.task.runtime.seconds" : meter .create_histogram (
317
+ name = "flower.task.runtime.seconds" ,
318
+ unit = "seconds" ,
319
+ description = "The time it took to run the task." ,
320
+ )
321
+ }
0 commit comments