|
1 | 1 | # (c) Copyright IBM Corp. 2025 |
2 | 2 |
|
3 | 3 | try: |
4 | | - from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple |
| 4 | + import inspect |
| 5 | + from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple |
5 | 6 |
|
6 | 7 | import kafka # noqa: F401 |
7 | 8 | import wrapt |
@@ -37,92 +38,118 @@ def trace_kafka_send( |
37 | 38 | span.set_attribute("kafka.access", "send") |
38 | 39 |
|
39 | 40 | # context propagation |
| 41 | + headers = kwargs.get("headers", []) |
40 | 42 | tracer.inject( |
41 | 43 | span.context, |
42 | 44 | Format.KAFKA_HEADERS, |
43 | | - kwargs.get("headers", {}), |
| 45 | + headers, |
44 | 46 | disable_w3c_trace_context=True, |
45 | 47 | ) |
46 | 48 |
|
47 | 49 | try: |
| 50 | + kwargs["headers"] = headers |
48 | 51 | res = wrapped(*args, **kwargs) |
49 | 52 | except Exception as exc: |
50 | 53 | span.record_exception(exc) |
51 | 54 | else: |
52 | 55 | return res |
53 | 56 |
|
54 | | - @wrapt.patch_function_wrapper("kafka", "KafkaConsumer.__next__") |
55 | | - def trace_kafka_consume( |
56 | | - wrapped: Callable[..., "kafka.KafkaConsumer.__next__"], |
57 | | - instance: "kafka.KafkaConsumer", |
58 | | - args: Tuple[int, str, Tuple[Any, ...]], |
59 | | - kwargs: Dict[str, Any], |
60 | | - ) -> "FutureRecordMetadata": |
61 | | - if tracing_is_off(): |
62 | | - return wrapped(*args, **kwargs) |
63 | | - |
| 57 | + def create_span( |
| 58 | + span_type: str, |
| 59 | + topic: Optional[str], |
| 60 | + headers: Optional[List[Tuple[str, bytes]]] = [], |
| 61 | + exception: Optional[str] = None, |
| 62 | + ) -> None: |
64 | 63 | tracer, parent_span, _ = get_tracer_tuple() |
65 | | - |
66 | 64 | parent_context = ( |
67 | 65 | parent_span.get_span_context() |
68 | 66 | if parent_span |
69 | 67 | else tracer.extract( |
70 | | - Format.KAFKA_HEADERS, {}, disable_w3c_trace_context=True |
| 68 | + Format.KAFKA_HEADERS, |
| 69 | + headers, |
| 70 | + disable_w3c_trace_context=True, |
71 | 71 | ) |
72 | 72 | ) |
73 | | - |
74 | 73 | with tracer.start_as_current_span( |
75 | 74 | "kafka-consumer", span_context=parent_context, kind=SpanKind.CONSUMER |
76 | 75 | ) as span: |
77 | | - topic = list(instance.subscription())[0] |
78 | | - span.set_attribute("kafka.service", topic) |
79 | | - span.set_attribute("kafka.access", "consume") |
| 76 | + if topic: |
| 77 | + span.set_attribute("kafka.service", topic) |
| 78 | + span.set_attribute("kafka.access", span_type) |
| 79 | + if exception: |
| 80 | + span.record_exception(exception) |
80 | 81 |
|
81 | | - try: |
82 | | - res = wrapped(*args, **kwargs) |
83 | | - except Exception as exc: |
84 | | - span.record_exception(exc) |
| 82 | + @wrapt.patch_function_wrapper("kafka", "KafkaConsumer.__next__") |
| 83 | + def trace_kafka_consume( |
| 84 | + wrapped: Callable[..., "kafka.KafkaConsumer.__next__"], |
| 85 | + instance: "kafka.KafkaConsumer", |
| 86 | + args: Tuple[int, str, Tuple[Any, ...]], |
| 87 | + kwargs: Dict[str, Any], |
| 88 | + ) -> "FutureRecordMetadata": |
| 89 | + if tracing_is_off(): |
| 90 | + return wrapped(*args, **kwargs) |
| 91 | + |
| 92 | + exception = None |
| 93 | + res = None |
| 94 | + |
| 95 | + try: |
| 96 | + res = wrapped(*args, **kwargs) |
| 97 | + except Exception as exc: |
| 98 | + exception = exc |
| 99 | + finally: |
| 100 | + if res: |
| 101 | + create_span( |
| 102 | + "consume", |
| 103 | + res.topic if res else list(instance.subscription())[0], |
| 104 | + res.headers, |
| 105 | + ) |
85 | 106 | else: |
86 | | - return res |
| 107 | + create_span( |
| 108 | + "consume", list(instance.subscription())[0], exception=exception |
| 109 | + ) |
| 110 | + |
| 111 | + return res |
87 | 112 |
|
88 | 113 | @wrapt.patch_function_wrapper("kafka", "KafkaConsumer.poll") |
89 | 114 | def trace_kafka_poll( |
90 | 115 | wrapped: Callable[..., "kafka.KafkaConsumer.poll"], |
91 | 116 | instance: "kafka.KafkaConsumer", |
92 | 117 | args: Tuple[int, str, Tuple[Any, ...]], |
93 | 118 | kwargs: Dict[str, Any], |
94 | | - ) -> Dict[str, Any]: |
| 119 | + ) -> Optional[Dict[str, Any]]: |
95 | 120 | if tracing_is_off(): |
96 | 121 | return wrapped(*args, **kwargs) |
97 | 122 |
|
98 | | - tracer, parent_span, _ = get_tracer_tuple() |
99 | | - |
100 | 123 | # The KafkaConsumer.consume() from the kafka-python-ng call the |
101 | 124 | # KafkaConsumer.poll() internally, so we do not consider it here. |
102 | | - if parent_span and parent_span.name == "kafka-consumer": |
| 125 | + if any( |
| 126 | + frame.function == "trace_kafka_consume" |
| 127 | + for frame in inspect.getouterframes(inspect.currentframe(), 2) |
| 128 | + ): |
103 | 129 | return wrapped(*args, **kwargs) |
104 | 130 |
|
105 | | - parent_context = ( |
106 | | - parent_span.get_span_context() |
107 | | - if parent_span |
108 | | - else tracer.extract( |
109 | | - Format.KAFKA_HEADERS, {}, disable_w3c_trace_context=True |
110 | | - ) |
111 | | - ) |
112 | | - |
113 | | - with tracer.start_as_current_span( |
114 | | - "kafka-consumer", span_context=parent_context, kind=SpanKind.CONSUMER |
115 | | - ) as span: |
116 | | - topic = list(instance.subscription())[0] |
117 | | - span.set_attribute("kafka.service", topic) |
118 | | - span.set_attribute("kafka.access", "poll") |
119 | | - |
120 | | - try: |
121 | | - res = wrapped(*args, **kwargs) |
122 | | - except Exception as exc: |
123 | | - span.record_exception(exc) |
| 131 | + exception = None |
| 132 | + res = None |
| 133 | + |
| 134 | + try: |
| 135 | + res = wrapped(*args, **kwargs) |
| 136 | + except Exception as exc: |
| 137 | + exception = exc |
| 138 | + finally: |
| 139 | + if res: |
| 140 | + for partition, consumer_records in res.items(): |
| 141 | + for message in consumer_records: |
| 142 | + create_span( |
| 143 | + "poll", |
| 144 | + partition.topic, |
| 145 | + message.headers if hasattr(message, "headers") else [], |
| 146 | + ) |
124 | 147 | else: |
125 | | - return res |
| 148 | + create_span( |
| 149 | + "poll", list(instance.subscription())[0], exception=exception |
| 150 | + ) |
| 151 | + |
| 152 | + return res |
126 | 153 |
|
127 | 154 | logger.debug("Instrumenting Kafka (kafka-python)") |
128 | 155 | except ImportError: |
|
0 commit comments