<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>article Monitoring Structured Streaming in Production with StreamingQueryListener in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/monitoring-structured-streaming-in-production-with/ba-p/117392</link>
    <description>&lt;H2&gt;&lt;SPAN&gt;Intro&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Databricks customers use Structured Streaming to drive critical business functions like equipment monitoring, fraud detection, and inventory management. Reliability is a key design factor for these workloads. Engineers design streaming jobs for consistent performance and minimal downtime. Performance monitoring allows engineers to proactively address issues before an outage or failure.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Monitoring streaming workloads, however, creates unique challenges. Streaming query performance often depends on several metrics. Engineers may want to understand the processed offsets, throughput, batch duration, or state memory when evaluating a streaming query. These metrics must be updated continuously with minimal impact on the workload’s performance.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; provides an extensible, fault-tolerant interface for monitoring streaming performance. In this blog, we’ll discuss the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; implementation and show how to set-up &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; for production jobs.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Streaming query metrics&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Every streaming query &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/structured-streaming/stream-monitoring#streamingquerylistener-object-metrics" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;creates metrics&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; with valuable information about streaming progress. This includes several identifiers—the query ID, run ID, and a configurable query name. After processing an incremental batch of data, queries emit default metrics like the batch duration and processed rows per second. A sample &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;QueryProgressEvent&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; is shown below.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;{
  "id" : "3574feba-646d-4735-83c4-66f657e52517",
  "runId" : "38a78903-9e55-4440-ad81-50b591e4746c",
  "name" : "STREAMING_QUERY_NAME_UNIQUE",
  "timestamp" : "2022-10-31T20:09:30.455Z",
  "batchId" : 1377,
  "numInputRows" : 687,
  "inputRowsPerSecond" : 32.13433743393049,
  "processedRowsPerSecond" : 34.067241892293964,
  "durationMs" : {
    "addBatch" : 18352,
    "getBatch" : 0,
    "latestOffset" : 31,
    "queryPlanning" : 977,
    "triggerExecution" : 20165,
    "walCommit" : 342
  },
...
}&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;QueryProgressEvents&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; also include details about the streaming source and sink. Kafka sources, for example, provide the latest processed and observed offsets for each partition and the number of offsets in the streaming backlog. We can use offset information to understand processing performance across each partition.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;{
...,
"sources" : [{
    "description" : "KafkaV2[Subscribe[KAFKA_TOPIC_001]]",
    "startOffset" : {
      "KAFKA_TOPIC_001" : {
        "0" : 349706380
      }
    },
    "endOffset" : {
      "KAFKA_TOPIC_001" : {
        "0" : 349706672
      }
    },
    "latestOffset" : {
      "KAFKA_TOPIC_001" : {
        "0" : 349706672
      }
    },
    "numInputRows" : 292,
    "inputRowsPerSecond" : 13.65826278123392,
    "processedRowsPerSecond" : 14.479817514628582,
    "metrics" : {
      "avgOffsetsBehindLatest" : "0.0",
      "estimatedTotalBytesBehindLatest" : "0.0",
      "maxOffsetsBehindLatest" : "0",
      "minOffsetsBehindLatest" : "0"
    }
  }]
}&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;By tracking these metrics over time, we can proactively improve streaming performance. We may, for example, identify increasing input volume by looking at the source offsets. To increase throughput, we could tune the cluster size or source configuration. To ensure a stateful query has bounded memory consumption, we could add a watermark to limit the data held in state.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Streaming query execution&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Streaming metrics can be processed by a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; during query execution. Each streaming query follows a consistent pattern: we define a streaming &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;DataFrame&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;, specify a sink format and options, and call &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;start()&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; to begin execution. Internally, Spark registers the stream in its &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryManager&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; and creates a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamExecution&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; object responsible for running the query.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Streaming query execution model" style="width: 809px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16385i178CFD7CBC1B364B/image-dimensions/809x385?v=v2" width="809" height="385" role="button" title="greghansen_0-1746105345098.png" alt="Streaming query execution model" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;Streaming query execution model&lt;/span&gt;&lt;/span&gt;&lt;SPAN&gt;When the stream is triggered, &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamExecution&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; processes an incremental batch of data from the source and creates a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;QueryProgressEvent&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. This event is asynchronously posted to Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListenerBus&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; where listeners are registered. Received events trigger each listener’s event handlers.&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="A streaming query listener event in Spark’s driver logs" style="width: 543px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16386i0A19B43381C93D80/image-dimensions/543x205?v=v2" width="543" height="205" role="button" title="greghansen_1-1746105345205.png" alt="A streaming query listener event in Spark’s driver logs" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;A streaming query listener event in Spark’s driver logs&lt;/span&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Each &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; runs on a driver thread and serially processes events received from the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListenerBus&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. This has several important consequences:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Failure of a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; will not disrupt the streaming query&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Failure of a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; will not disrupt another &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;When a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; cannot keep up with arriving listener events, those events will be dropped&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;Because the Spark driver is responsible for executing &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; methods, listeners with complex event-handling logic may overwhelm the driver’s resources.&amp;nbsp; By keeping our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; implementation lightweight, we can continuously monitor streaming metrics and avoid impacting the ongoing streaming query.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Creating a StreamingQueryListener&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Streaming listeners extend the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; interface. A listener must implement methods to handle various query progress events.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from pyspark.sql.streaming import StreamingQueryListener
from pyspark.sql.streaming.listener import QueryStartedEvent, QueryProgressEvent, QueryTerminatedEvent

class EventHubListener(StreamingQueryListener):
    ...
    def onQueryStarted(self, event: QueryStartedEvent):
        # Handles a QueryStartedEvent when the stream starts
        ...

    def onQueryProgress(self, event: QueryProgressEvent):
        # Handles a QueryProgressEvent whenever a streaming batch finishes
        ...

    def onQueryTerminated(self, event: QueryTerminatedEvent):
        # Handles a QueryTerminatedEvent when the stream is stopped or cancelled
        ...&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;Production streaming query listeners should avoid complex processing logic and forward metrics to low-latency sinks like queues or logging services. This reduces resource contention and ensures minimal impact on the streaming query performance. The example below forwards streaming metrics to an Azure Event Hub using a static &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;EventHubProducerClient&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from pyspark.sql.streaming import StreamingQueryListener
from pyspark.sql.streaming.listener import QueryStartedEvent, QueryProgressEvent, QueryTerminatedEvent
from azure.eventhub import EventHubProducerClient, EventData

class EventHubQueryListener(StreamingQueryListener):
  """ Forwards structured streaming query metrics to Azure Event Hubs."""
  _connection_string: str
  _event_hub_name: str
  _event_hub_client: EventHubProducerClient

  def __init__(self, connection_string: str, event_hub_name: str):
    """ Initializes the EventHubQueryListener by creating the Event Hub client."""
    self._connection_string = connection_string
    self._event_hub_name = event_hub_name
    self._event_hub_client = EventHubProducerClient.from_connection_string(
      conn_str=self._connection_string,
      eventhub_name=self._event_hub_name
    )

  def sendMessage(self, message: str):
    """ Structures and sends a message to Event Hub."""
    event_batch = self._event_hub_client.create_batch()
    event_batch.add(EventData(message))
    self._event_hub_client.send_batch(event_batch)
  
  def onQueryProgress(self, event: QueryProgressEvent):
    """ Sends an Event Hub message when a micro batch completes."""
    self.sendMessage(str(event.progress))

  ...&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; subclasses can be added in code to the listener bus using Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryManager&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. Because listeners process events asynchronously, they can be added before, during, or after a stream is started.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# Instantiate the listener:
connection_string = "..."
event_hub_name = "..."
event_hub_listener = EventHubListener(connection_string, event_hub_name)

# Register the listener using the StreamingQueryManager:
spark.streams.addListener(event_hub_listener)&lt;/LI-CODE&gt;
&lt;H2&gt;&lt;SPAN&gt;Standardizing StreamingQueryListeners across workloads&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;By reusing a shared streaming listener implementation, operations teams can achieve consistent observability across streaming workloads with minimal development overhead. This is especially useful for teams monitoring many streaming queries. Streaming metrics can be incorporated into a larger monitoring process. We could, for example:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Read listener metrics from multiple streams from a single Event Hub&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Process and write the metrics to a Delta table in Unity Catalog&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Use &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/sql/user/alerts/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Databricks’ SQL alerts&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/dashboards/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;AI/BI dashboards&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; for real-time incident response and observability&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="An architecture for centralizing performance metrics using a shared StreamingQueryListener" style="width: 797px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16387iE5BA597493FFB8B3/image-dimensions/797x250?v=v2" width="797" height="250" role="button" title="greghansen_2-1746105345242.png" alt="An architecture for centralizing performance metrics using a shared StreamingQueryListener" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;An architecture for centralizing performance metrics using a shared StreamingQueryListener&lt;/span&gt;&lt;/span&gt;&lt;SPAN&gt;When we add our listener class to our cluster’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;spark.sql.streaming.streamingQueryListeners&lt;/FONT&gt;&lt;SPAN&gt;, it will be registered by default whenever a notebook or job is attached to the cluster. Our custom &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;StreamingQueryListener&lt;/FONT&gt;&lt;SPAN&gt; class will provide out-of-the-box observability without requiring engineers to implement or register listeners in code!&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Creating a StreamingQueryListener JAR&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;To register our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;EventHubListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; as a Spark listener, we first need to package the subclass as a JAR. Most Java or Scala IDEs have tooling to streamline this process. We can use Maven to package our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;EventHubListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; into a JAR with all the necessary dependencies.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Packaging a JAR with Maven relies on configuration in a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;pom.xml&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; file. This includes information about the project including its target runtime environment and dependencies. When packaging a JAR for use on Databricks:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Compile against the Spark, Java, and Scala versions used in your cluster’s &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/release-notes/runtime/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Databricks runtime version&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to ensure compatibility&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Set any dependencies provided by the Databricks Runtime to have a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;provided&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; scope to minimize the JAR size&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Ensure you &lt;/SPAN&gt;&lt;A href="https://support.sonatype.com/hc/en-us/articles/28958118202131-What-is-an-uber-jar" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;include any necessary dependencies&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; when creating the JAR&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;Once you’ve created your JAR, you can upload it to a Unity Catalog volume or as a file in your Databricks workspace. The JAR can be installed on Databricks clusters as a cluster-scoped library or by using a cluster-scoped init script.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Loading the JAR into Spark’s class path&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Databricks’ Spark environment is pre-loaded with JARs providing various functionality. To use our listener class, we need to load the JAR into the Spark class path using a basic shell command.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="markup"&gt;# ------------------------------------------------------
# Copies the EventHubListener JAR into /databricks/jars.
# This ensures our listener class is loaded in the Spark
# environment when we start a Databricks cluster.
# ------------------------------------------------------

cp /Volumes/org/streaming/artifacts/java/jars/eventhublistener-1.0-SNAPSHOT-jar-with-dependencies.jar /databricks/jars&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;Our script can be uploaded to a Unity Catalog volume or as a workspace file. We can set this as a cluster-scoped &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/init-scripts/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;init script&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; in the advanced settings of our cluster configuration.&lt;/SPAN&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Setting up an init script using the cluster configuration UI" style="width: 607px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16388i9E33A8A75E1D5A43/image-dimensions/607x115?v=v2" width="607" height="115" role="button" title="greghansen_3-1746105345235.png" alt="Setting up an init script using the cluster configuration UI" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;Setting up an init script using the cluster configuration UI&lt;/span&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Setting the Spark configuration&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;We can now add our listener to the default list of streaming query listeners. We can set the&amp;nbsp; &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;spark.sql.streaming.streamingQueryListeners &amp;lt;fully qualified class name&amp;gt; &lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;to our cluster’s Spark configuration.&lt;/SPAN&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Adding a default listener to the Spark configuration" style="width: 428px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16389iEA7D6DB706E43887/image-dimensions/428x140?v=v2" width="428" height="140" role="button" title="greghansen_4-1746105345238.png" alt="Adding a default listener to the Spark configuration" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;Adding a default listener to the Spark configuration&lt;/span&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;SPAN&gt;Our listener will be added to the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListenerBus&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; whenever a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;SparkSession&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; is created!&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Creating a compute policy&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Databricks &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/admin/clusters/policies" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;compute policies&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; control configuration for Databricks clusters created in the web portal or with Databricks Asset Bundles. Our init script and Spark configuration can be added as fixed values in the policy settings.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;{
  "init_scripts.0.workspace.destination": {
    "type": "fixed",
    "value": "/Users/gregory.hansen@databricks.com/install_streaming_listener.sh"
  },
  "spark_conf.spark.sql.streaming.streamingQueryListeners": {
    "type": "fixed",
    "value": "com.dbfe.EventHubListener"
  }
}&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;When a Databricks cluster is created with this compute policy, the cluster will automatically use the init script and Spark configuration needed to install our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. Streaming queries will forward metrics to our monitoring Event Hub by default. These metrics can be standardized and monitored across streaming jobs for an entire organization!&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Conclusion&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Monitoring streaming jobs is critical to ensuring workloads run reliably. Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; provides a low-impact, fault-tolerant solution for forwarding metrics to event queues or logging services. By standardizing streaming query listeners across job deployments, we can minimize development overhead, centralize metrics, and create a complete view of streaming performance across an organization.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Check out our &lt;/SPAN&gt;&lt;A href="https://github.com/databricks-solutions/databricks-blogposts/tree/main/2025-04-monitoring-structured-streaming-in-production" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Git repo&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; for an end-to-end example forwarding &lt;FONT face="courier new,courier"&gt;StreamingQueryListener&lt;/FONT&gt; metrics to Azure Event Hubs. For more on streaming observability, check out our &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/announcing-state-reader-api-new-statestore-data-source" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;product blog on the StateReader API&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;</description>
    <pubDate>Wed, 07 May 2025 11:17:02 GMT</pubDate>
    <dc:creator>greg-hansen</dc:creator>
    <dc:date>2025-05-07T11:17:02Z</dc:date>
    <item>
      <title>Monitoring Structured Streaming in Production with StreamingQueryListener</title>
      <link>https://community.databricks.com/t5/technical-blog/monitoring-structured-streaming-in-production-with/ba-p/117392</link>
      <description>&lt;H2&gt;&lt;SPAN&gt;Intro&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Databricks customers use Structured Streaming to drive critical business functions like equipment monitoring, fraud detection, and inventory management. Reliability is a key design factor for these workloads. Engineers design streaming jobs for consistent performance and minimal downtime. Performance monitoring allows engineers to proactively address issues before an outage or failure.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Monitoring streaming workloads, however, creates unique challenges. Streaming query performance often depends on several metrics. Engineers may want to understand the processed offsets, throughput, batch duration, or state memory when evaluating a streaming query. These metrics must be updated continuously with minimal impact on the workload’s performance.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; provides an extensible, fault-tolerant interface for monitoring streaming performance. In this blog, we’ll discuss the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; implementation and show how to set-up &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; for production jobs.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Streaming query metrics&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Every streaming query &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/structured-streaming/stream-monitoring#streamingquerylistener-object-metrics" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;creates metrics&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; with valuable information about streaming progress. This includes several identifiers—the query ID, run ID, and a configurable query name. After processing an incremental batch of data, queries emit default metrics like the batch duration and processed rows per second. A sample &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;QueryProgressEvent&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; is shown below.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;{
  "id" : "3574feba-646d-4735-83c4-66f657e52517",
  "runId" : "38a78903-9e55-4440-ad81-50b591e4746c",
  "name" : "STREAMING_QUERY_NAME_UNIQUE",
  "timestamp" : "2022-10-31T20:09:30.455Z",
  "batchId" : 1377,
  "numInputRows" : 687,
  "inputRowsPerSecond" : 32.13433743393049,
  "processedRowsPerSecond" : 34.067241892293964,
  "durationMs" : {
    "addBatch" : 18352,
    "getBatch" : 0,
    "latestOffset" : 31,
    "queryPlanning" : 977,
    "triggerExecution" : 20165,
    "walCommit" : 342
  },
...
}&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;QueryProgressEvents&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; also include details about the streaming source and sink. Kafka sources, for example, provide the latest processed and observed offsets for each partition and the number of offsets in the streaming backlog. We can use offset information to understand processing performance across each partition.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;{
...,
"sources" : [{
    "description" : "KafkaV2[Subscribe[KAFKA_TOPIC_001]]",
    "startOffset" : {
      "KAFKA_TOPIC_001" : {
        "0" : 349706380
      }
    },
    "endOffset" : {
      "KAFKA_TOPIC_001" : {
        "0" : 349706672
      }
    },
    "latestOffset" : {
      "KAFKA_TOPIC_001" : {
        "0" : 349706672
      }
    },
    "numInputRows" : 292,
    "inputRowsPerSecond" : 13.65826278123392,
    "processedRowsPerSecond" : 14.479817514628582,
    "metrics" : {
      "avgOffsetsBehindLatest" : "0.0",
      "estimatedTotalBytesBehindLatest" : "0.0",
      "maxOffsetsBehindLatest" : "0",
      "minOffsetsBehindLatest" : "0"
    }
  }]
}&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;By tracking these metrics over time, we can proactively improve streaming performance. We may, for example, identify increasing input volume by looking at the source offsets. To increase throughput, we could tune the cluster size or source configuration. To ensure a stateful query has bounded memory consumption, we could add a watermark to limit the data held in state.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Streaming query execution&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Streaming metrics can be processed by a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; during query execution. Each streaming query follows a consistent pattern: we define a streaming &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;DataFrame&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;, specify a sink format and options, and call &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;start()&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; to begin execution. Internally, Spark registers the stream in its &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryManager&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; and creates a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamExecution&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; object responsible for running the query.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Streaming query execution model" style="width: 809px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16385i178CFD7CBC1B364B/image-dimensions/809x385?v=v2" width="809" height="385" role="button" title="greghansen_0-1746105345098.png" alt="Streaming query execution model" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;Streaming query execution model&lt;/span&gt;&lt;/span&gt;&lt;SPAN&gt;When the stream is triggered, &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamExecution&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; processes an incremental batch of data from the source and creates a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;QueryProgressEvent&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. This event is asynchronously posted to Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListenerBus&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; where listeners are registered. Received events trigger each listener’s event handlers.&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="A streaming query listener event in Spark’s driver logs" style="width: 543px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16386i0A19B43381C93D80/image-dimensions/543x205?v=v2" width="543" height="205" role="button" title="greghansen_1-1746105345205.png" alt="A streaming query listener event in Spark’s driver logs" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;A streaming query listener event in Spark’s driver logs&lt;/span&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Each &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; runs on a driver thread and serially processes events received from the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListenerBus&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. This has several important consequences:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Failure of a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; will not disrupt the streaming query&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Failure of a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; will not disrupt another &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;When a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; cannot keep up with arriving listener events, those events will be dropped&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;Because the Spark driver is responsible for executing &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; methods, listeners with complex event-handling logic may overwhelm the driver’s resources.&amp;nbsp; By keeping our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; implementation lightweight, we can continuously monitor streaming metrics and avoid impacting the ongoing streaming query.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Creating a StreamingQueryListener&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Streaming listeners extend the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; interface. A listener must implement methods to handle various query progress events.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from pyspark.sql.streaming import StreamingQueryListener
from pyspark.sql.streaming.listener import QueryStartedEvent, QueryProgressEvent, QueryTerminatedEvent

class EventHubListener(StreamingQueryListener):
    ...
    def onQueryStarted(self, event: QueryStartedEvent):
        # Handles a QueryStartedEvent when the stream starts
        ...

    def onQueryProgress(self, event: QueryProgressEvent):
        # Handles a QueryProgressEvent whenever a streaming batch finishes
        ...

    def onQueryTerminated(self, event: QueryTerminatedEvent):
        # Handles a QueryTerminatedEvent when the stream is stopped or cancelled
        ...&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;Production streaming query listeners should avoid complex processing logic and forward metrics to low-latency sinks like queues or logging services. This reduces resource contention and ensures minimal impact on the streaming query performance. The example below forwards streaming metrics to an Azure Event Hub using a static &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;EventHubProducerClient&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from pyspark.sql.streaming import StreamingQueryListener
from pyspark.sql.streaming.listener import QueryStartedEvent, QueryProgressEvent, QueryTerminatedEvent
from azure.eventhub import EventHubProducerClient, EventData

class EventHubQueryListener(StreamingQueryListener):
  """ Forwards structured streaming query metrics to Azure Event Hubs."""
  _connection_string: str
  _event_hub_name: str
  _event_hub_client: EventHubProducerClient

  def __init__(self, connection_string: str, event_hub_name: str):
    """ Initializes the EventHubQueryListener by creating the Event Hub client."""
    self._connection_string = connection_string
    self._event_hub_name = event_hub_name
    self._event_hub_client = EventHubProducerClient.from_connection_string(
      conn_str=self._connection_string,
      eventhub_name=self._event_hub_name
    )

  def sendMessage(self, message: str):
    """ Structures and sends a message to Event Hub."""
    event_batch = self._event_hub_client.create_batch()
    event_batch.add(EventData(message))
    self._event_hub_client.send_batch(event_batch)
  
  def onQueryProgress(self, event: QueryProgressEvent):
    """ Sends an Event Hub message when a micro batch completes."""
    self.sendMessage(str(event.progress))

  ...&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; subclasses can be added in code to the listener bus using Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryManager&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. Because listeners process events asynchronously, they can be added before, during, or after a stream is started.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# Instantiate the listener:
connection_string = "..."
event_hub_name = "..."
event_hub_listener = EventHubListener(connection_string, event_hub_name)

# Register the listener using the StreamingQueryManager:
spark.streams.addListener(event_hub_listener)&lt;/LI-CODE&gt;
&lt;H2&gt;&lt;SPAN&gt;Standardizing StreamingQueryListeners across workloads&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;By reusing a shared streaming listener implementation, operations teams can achieve consistent observability across streaming workloads with minimal development overhead. This is especially useful for teams monitoring many streaming queries. Streaming metrics can be incorporated into a larger monitoring process. We could, for example:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Read listener metrics from multiple streams from a single Event Hub&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Process and write the metrics to a Delta table in Unity Catalog&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Use &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/sql/user/alerts/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Databricks’ SQL alerts&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; and &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/dashboards/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;AI/BI dashboards&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; for real-time incident response and observability&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="An architecture for centralizing performance metrics using a shared StreamingQueryListener" style="width: 797px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16387iE5BA597493FFB8B3/image-dimensions/797x250?v=v2" width="797" height="250" role="button" title="greghansen_2-1746105345242.png" alt="An architecture for centralizing performance metrics using a shared StreamingQueryListener" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;An architecture for centralizing performance metrics using a shared StreamingQueryListener&lt;/span&gt;&lt;/span&gt;&lt;SPAN&gt;When we add our listener class to our cluster’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;spark.sql.streaming.streamingQueryListeners&lt;/FONT&gt;&lt;SPAN&gt;, it will be registered by default whenever a notebook or job is attached to the cluster. Our custom &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;StreamingQueryListener&lt;/FONT&gt;&lt;SPAN&gt; class will provide out-of-the-box observability without requiring engineers to implement or register listeners in code!&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Creating a StreamingQueryListener JAR&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;To register our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;EventHubListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; as a Spark listener, we first need to package the subclass as a JAR. Most Java or Scala IDEs have tooling to streamline this process. We can use Maven to package our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;EventHubListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; into a JAR with all the necessary dependencies.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Packaging a JAR with Maven relies on configuration in a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;pom.xml&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; file. This includes information about the project including its target runtime environment and dependencies. When packaging a JAR for use on Databricks:&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Compile against the Spark, Java, and Scala versions used in your cluster’s &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/release-notes/runtime/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Databricks runtime version&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to ensure compatibility&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Set any dependencies provided by the Databricks Runtime to have a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;provided&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; scope to minimize the JAR size&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;Ensure you &lt;/SPAN&gt;&lt;A href="https://support.sonatype.com/hc/en-us/articles/28958118202131-What-is-an-uber-jar" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;include any necessary dependencies&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; when creating the JAR&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN&gt;Once you’ve created your JAR, you can upload it to a Unity Catalog volume or as a file in your Databricks workspace. The JAR can be installed on Databricks clusters as a cluster-scoped library or by using a cluster-scoped init script.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Loading the JAR into Spark’s class path&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Databricks’ Spark environment is pre-loaded with JARs providing various functionality. To use our listener class, we need to load the JAR into the Spark class path using a basic shell command.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="markup"&gt;# ------------------------------------------------------
# Copies the EventHubListener JAR into /databricks/jars.
# This ensures our listener class is loaded in the Spark
# environment when we start a Databricks cluster.
# ------------------------------------------------------

cp /Volumes/org/streaming/artifacts/java/jars/eventhublistener-1.0-SNAPSHOT-jar-with-dependencies.jar /databricks/jars&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;Our script can be uploaded to a Unity Catalog volume or as a workspace file. We can set this as a cluster-scoped &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/init-scripts/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;init script&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; in the advanced settings of our cluster configuration.&lt;/SPAN&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Setting up an init script using the cluster configuration UI" style="width: 607px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16388i9E33A8A75E1D5A43/image-dimensions/607x115?v=v2" width="607" height="115" role="button" title="greghansen_3-1746105345235.png" alt="Setting up an init script using the cluster configuration UI" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;Setting up an init script using the cluster configuration UI&lt;/span&gt;&lt;/span&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Setting the Spark configuration&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;We can now add our listener to the default list of streaming query listeners. We can set the&amp;nbsp; &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;spark.sql.streaming.streamingQueryListeners &amp;lt;fully qualified class name&amp;gt; &lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;to our cluster’s Spark configuration.&lt;/SPAN&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Adding a default listener to the Spark configuration" style="width: 428px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/16389iEA7D6DB706E43887/image-dimensions/428x140?v=v2" width="428" height="140" role="button" title="greghansen_4-1746105345238.png" alt="Adding a default listener to the Spark configuration" /&gt;&lt;span class="lia-inline-image-caption" onclick="event.preventDefault();"&gt;Adding a default listener to the Spark configuration&lt;/span&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;SPAN&gt;Our listener will be added to the &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListenerBus&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; whenever a &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;SparkSession&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; is created!&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN&gt;Creating a compute policy&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN&gt;Databricks &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/admin/clusters/policies" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;compute policies&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; control configuration for Databricks clusters created in the web portal or with Databricks Asset Bundles. Our init script and Spark configuration can be added as fixed values in the policy settings.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;{
  "init_scripts.0.workspace.destination": {
    "type": "fixed",
    "value": "/Users/gregory.hansen@databricks.com/install_streaming_listener.sh"
  },
  "spark_conf.spark.sql.streaming.streamingQueryListeners": {
    "type": "fixed",
    "value": "com.dbfe.EventHubListener"
  }
}&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;When a Databricks cluster is created with this compute policy, the cluster will automatically use the init script and Spark configuration needed to install our &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt;. Streaming queries will forward metrics to our monitoring Event Hub by default. These metrics can be standardized and monitored across streaming jobs for an entire organization!&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN&gt;Conclusion&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Monitoring streaming jobs is critical to ensuring workloads run reliably. Spark’s &lt;/SPAN&gt;&lt;FONT face="courier new,courier"&gt;&lt;SPAN&gt;StreamingQueryListener&lt;/SPAN&gt;&lt;/FONT&gt;&lt;SPAN&gt; provides a low-impact, fault-tolerant solution for forwarding metrics to event queues or logging services. By standardizing streaming query listeners across job deployments, we can minimize development overhead, centralize metrics, and create a complete view of streaming performance across an organization.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Check out our &lt;/SPAN&gt;&lt;A href="https://github.com/databricks-solutions/databricks-blogposts/tree/main/2025-04-monitoring-structured-streaming-in-production" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Git repo&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; for an end-to-end example forwarding &lt;FONT face="courier new,courier"&gt;StreamingQueryListener&lt;/FONT&gt; metrics to Azure Event Hubs. For more on streaming observability, check out our &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/announcing-state-reader-api-new-statestore-data-source" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;product blog on the StateReader API&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;</description>
      <pubDate>Wed, 07 May 2025 11:17:02 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/monitoring-structured-streaming-in-production-with/ba-p/117392</guid>
      <dc:creator>greg-hansen</dc:creator>
      <dc:date>2025-05-07T11:17:02Z</dc:date>
    </item>
  </channel>
</rss>

