1313# limitations under the License.
1414import logging
1515import sys
16+ import threading
17+ import weakref
1618
1719from newrelic .api .application import application_instance
1820from newrelic .api .error_trace import wrap_error_trace
3335HEARTBEAT_SESSION_TIMEOUT = "MessageBroker/Kafka/Heartbeat/SessionTimeout"
3436HEARTBEAT_POLL_TIMEOUT = "MessageBroker/Kafka/Heartbeat/PollTimeout"
3537
38+ KAFKA_CLUSTER_METRIC_PRODUCE = "MessageBroker/Kafka/Cluster/{0}/Topic/{1}/Produce"
39+ KAFKA_CLUSTER_METRIC_CONSUME = "MessageBroker/Kafka/Cluster/{0}/Topic/{1}/Consume"
40+
41+
42+ _nr_cluster_id_cache = {}
43+ _nr_cluster_id_cache_lock = threading .Lock ()
44+
45+
46+ def _fetch_cluster_id (instance ):
47+ servers = getattr (instance , "_nr_bootstrap_servers" , None )
48+ # Sort so that equivalent broker sets with different orderings share the same key.
49+ cache_key = "," .join (sorted (servers )) if servers else None
50+
51+ if cache_key :
52+ with _nr_cluster_id_cache_lock :
53+ cached = _nr_cluster_id_cache .get (cache_key )
54+ if cached :
55+ instance ._nr_cluster_id = cached
56+ return
57+ if cached is not None :
58+ return
59+ _nr_cluster_id_cache [cache_key ] = ""
60+
61+ # Hold only a weak reference so the thread closure does not extend the
62+ # lifetime of a Producer/Consumer that the caller has already abandoned.
63+ instance_ref = weakref .ref (instance )
64+
65+ def _run ():
66+ inst = instance_ref ()
67+ if inst is None :
68+ # Instance was GC'd before the thread ran; clean up sentinel and exit.
69+ if cache_key :
70+ with _nr_cluster_id_cache_lock :
71+ _nr_cluster_id_cache .pop (cache_key , None )
72+ return
73+ try :
74+ meta = inst .list_topics (timeout = 5 )
75+ cluster_id = getattr (meta , "cluster_id" , None )
76+ if cluster_id :
77+ inst ._nr_cluster_id = cluster_id
78+ if cache_key :
79+ with _nr_cluster_id_cache_lock :
80+ _nr_cluster_id_cache [cache_key ] = cluster_id
81+ except Exception as e :
82+ _logger .debug ("NR Kafka cluster ID fetch failed" , exc_info = True )
83+ if cache_key :
84+ with _nr_cluster_id_cache_lock :
85+ _nr_cluster_id_cache .pop (cache_key , None )
86+
87+ threading .Thread (target = _run , daemon = True , name = "NR-Kafka-ClusterId" ).start ()
88+
3689
3790def wrap_Producer_produce (wrapped , instance , args , kwargs ):
3891 transaction = current_transaction ()
@@ -63,6 +116,17 @@ def wrap_Producer_produce(wrapped, instance, args, kwargs):
63116 for server_name in instance ._nr_bootstrap_servers :
64117 transaction .record_custom_metric (f"MessageBroker/Kafka/Nodes/{ server_name } /Produce/{ topic } " , 1 )
65118
119+ cluster_id = getattr (instance , "_nr_cluster_id" , None )
120+ if not cluster_id and hasattr (instance , "_nr_bootstrap_servers" ):
121+ _cache_key = "," .join (sorted (instance ._nr_bootstrap_servers ))
122+ cluster_id = _nr_cluster_id_cache .get (_cache_key ) or None
123+ if cluster_id :
124+ instance ._nr_cluster_id = cluster_id # cache on instance for future calls
125+ if cluster_id :
126+ transaction .record_custom_metric (
127+ KAFKA_CLUSTER_METRIC_PRODUCE .format (cluster_id , topic ), 1
128+ )
129+
66130 with MessageTrace (
67131 library = "Kafka" , operation = "Produce" , destination_type = "Topic" , destination_name = topic , source = wrapped
68132 ):
@@ -171,6 +235,16 @@ def wrap_Consumer_poll(wrapped, instance, args, kwargs):
171235 transaction .record_custom_metric (
172236 f"MessageBroker/Kafka/Nodes/{ server_name } /Consume/{ destination_name } " , 1
173237 )
238+ cluster_id = getattr (instance , "_nr_cluster_id" , None )
239+ if not cluster_id and hasattr (instance , "_nr_bootstrap_servers" ):
240+ _cache_key = "," .join (sorted (instance ._nr_bootstrap_servers ))
241+ cluster_id = _nr_cluster_id_cache .get (_cache_key ) or None
242+ if cluster_id :
243+ instance ._nr_cluster_id = cluster_id
244+ if cluster_id :
245+ transaction .record_custom_metric (
246+ KAFKA_CLUSTER_METRIC_CONSUME .format (cluster_id , destination_name ), 1
247+ )
174248 transaction .add_messagebroker_info ("Confluent-Kafka" , get_package_version ("confluent-kafka" ))
175249
176250 return record
@@ -213,6 +287,16 @@ def wrap_SerializingProducer_init(wrapped, instance, args, kwargs):
213287 if hasattr (instance , "_value_serializer" ) and callable (instance ._value_serializer ):
214288 instance ._value_serializer = wrap_serializer ("Serialization/Value" , "MessageBroker" )(instance ._value_serializer )
215289
290+ try :
291+ conf = kwargs .get ("conf" ) or (args [0 ] if args else {})
292+ servers = conf .get ("bootstrap.servers" ) if isinstance (conf , dict ) else None
293+ if servers :
294+ instance ._nr_bootstrap_servers = servers .split ("," )
295+ except Exception :
296+ pass
297+
298+ _fetch_cluster_id (instance )
299+
216300
217301def wrap_DeserializingConsumer_init (wrapped , instance , args , kwargs ):
218302 wrapped (* args , ** kwargs )
@@ -223,6 +307,16 @@ def wrap_DeserializingConsumer_init(wrapped, instance, args, kwargs):
223307 if hasattr (instance , "_value_deserializer" ) and callable (instance ._value_deserializer ):
224308 instance ._value_deserializer = wrap_serializer ("Deserialization/Value" , "Message" )(instance ._value_deserializer )
225309
310+ try :
311+ conf = kwargs .get ("conf" ) or (args [0 ] if args else {})
312+ servers = conf .get ("bootstrap.servers" ) if isinstance (conf , dict ) else None
313+ if servers :
314+ instance ._nr_bootstrap_servers = servers .split ("," )
315+ except Exception :
316+ pass
317+
318+ _fetch_cluster_id (instance )
319+
226320
227321def wrap_Producer_init (wrapped , instance , args , kwargs ):
228322 wrapped (* args , ** kwargs )
@@ -236,6 +330,8 @@ def wrap_Producer_init(wrapped, instance, args, kwargs):
236330 except Exception :
237331 pass
238332
333+ _fetch_cluster_id (instance )
334+
239335
240336def wrap_Consumer_init (wrapped , instance , args , kwargs ):
241337 wrapped (* args , ** kwargs )
@@ -249,6 +345,8 @@ def wrap_Consumer_init(wrapped, instance, args, kwargs):
249345 except Exception :
250346 pass
251347
348+ _fetch_cluster_id (instance )
349+
252350
253351def wrap_immutable_class (module , class_name ):
254352 # Wrap immutable binary extension class with a mutable Python subclass
0 commit comments