<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aykut Bulgu</title>
    <description>The latest articles on DEV Community by Aykut Bulgu (@mabulgu).</description>
    <link>https://dev.to/mabulgu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038831%2F5433e397-1032-4a5b-9a63-9f9e5300f5e5.png</url>
      <title>DEV Community: Aykut Bulgu</title>
      <link>https://dev.to/mabulgu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mabulgu"/>
    <language>en</language>
    <item>
      <title>How to Auto-Create Jira Issues from Event Streams with Redpanda Connect</title>
      <dc:creator>Aykut Bulgu</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:17:04 +0000</pubDate>
      <link>https://dev.to/mabulgu/how-to-auto-create-jira-issues-from-event-streams-with-redpanda-connect-5c0f</link>
      <guid>https://dev.to/mabulgu/how-to-auto-create-jira-issues-from-event-streams-with-redpanda-connect-5c0f</guid>
      <description>&lt;p&gt;Your application fires hundreds of events per minute. Somewhere in that stream is a critical error that needs a Jira ticket. Someone watches a dashboard, decides the spike matters, and creates the ticket by hand. By the time the ticket exists, the incident is already twenty minutes old.&lt;/p&gt;

&lt;p&gt;Redpanda Connect can close this gap. I wanted to see if I could get from a critical event on the topic to a ticket in Jira without writing a custom service. By combining its &lt;code&gt;http&lt;/code&gt; processor with Bloblang mappings, you can wire your event stream directly to Jira's REST API so qualifying events create issues automatically. The filter logic, field mapping, and API call all live in a single declarative YAML file you can version-control and deploy alongside the rest of your infrastructure.&lt;/p&gt;

&lt;p&gt;Read on to learn how I put that together, and specifically how you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consume application error events from a Redpanda topic using Redpanda Connect&lt;/li&gt;
&lt;li&gt;Filter for critical-severity events using Bloblang mappings&lt;/li&gt;
&lt;li&gt;Shape the filtered events into Jira REST API payloads&lt;/li&gt;
&lt;li&gt;Create Jira issues automatically using the &lt;code&gt;http&lt;/code&gt; processor with Basic Auth&lt;/li&gt;
&lt;li&gt;Verify the end-to-end pipeline by producing test events and checking Jira&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Auto-Creating Jira Issues from Redpanda Event Streams
&lt;/h2&gt;

&lt;p&gt;Suppose you work for a company that runs multiple backend services behind a shared API gateway. Each service publishes structured JSON error events to a dedicated &lt;code&gt;application-errors&lt;/code&gt; topic on Redpanda. The events carry a &lt;code&gt;severity&lt;/code&gt; field (&lt;code&gt;info&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, or &lt;code&gt;critical&lt;/code&gt;), a &lt;code&gt;service&lt;/code&gt; name, an error &lt;code&gt;message&lt;/code&gt;, and a &lt;code&gt;timestamp&lt;/code&gt;. The operations team wants every critical error to appear as a Jira ticket within seconds, without anyone watching a dashboard or running a script.&lt;/p&gt;

&lt;p&gt;The pipeline does four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consumes all events from the &lt;code&gt;application-errors&lt;/code&gt; topic.&lt;/li&gt;
&lt;li&gt;Drops everything that is not &lt;code&gt;critical&lt;/code&gt; severity.&lt;/li&gt;
&lt;li&gt;Transforms the remaining events into the JSON structure Jira's REST API expects.&lt;/li&gt;
&lt;li&gt;Sends a POST request to Jira to create the issue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgc7mxaeavynhjnainw6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgc7mxaeavynhjnainw6h.png" alt="Architecture diagram" width="800" height="40"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Redpanda Connect keeps the routing, filtering, and transformation in one pipeline config. The &lt;code&gt;http&lt;/code&gt; processor handles authentication and the API call, while the &lt;code&gt;mapping&lt;/code&gt; processors handle the filtering and structural translation between your event schema and Jira's expected fields.&lt;/p&gt;

&lt;p&gt;Redpanda Connect also includes a dedicated &lt;code&gt;jira&lt;/code&gt; processor for querying Jira resources (searching issues by JQL, listing projects, retrieving transitions). I'm using the &lt;code&gt;http&lt;/code&gt; processor instead because creating issues requires a write operation against the Jira REST API, which the &lt;code&gt;jira&lt;/code&gt; processor does not support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before you start, make sure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A running Redpanda cluster. You can spin one up with Docker by following the &lt;a href="https://docs.redpanda.com/docs/quickstart/quick-start-docker/" rel="noopener noreferrer"&gt;Docker quickstart&lt;/a&gt;, or use any OS via the &lt;a href="https://docs.redpanda.com/docs/quickstart/" rel="noopener noreferrer"&gt;general quickstart&lt;/a&gt;. If you prefer a hosted cluster, &lt;a href="https://docs.redpanda.com/cloud-data-platform/get-started/cluster-types/serverless" rel="noopener noreferrer"&gt;Redpanda Serverless&lt;/a&gt; works as well.&lt;/li&gt;
&lt;li&gt;Redpanda Connect installed. Run &lt;code&gt;rpk connect --version&lt;/code&gt; to confirm. If &lt;code&gt;rpk&lt;/code&gt; is not installed yet, see the &lt;a href="https://docs.redpanda.com/redpanda-connect/home/" rel="noopener noreferrer"&gt;rpk installation docs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A Jira Cloud account with API token access. Generate a token at &lt;a href="https://id.atlassian.com/manage-profile/security/api-tokens" rel="noopener noreferrer"&gt;id.atlassian.com/manage-profile/security/api-tokens&lt;/a&gt;. You'll need your Jira base URL (e.g. &lt;code&gt;https://your-org.atlassian.net&lt;/code&gt;), account email, and that token.&lt;/li&gt;
&lt;li&gt;Python 3.8 or later with &lt;code&gt;pip&lt;/code&gt; for running the test event producer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuring the Input
&lt;/h3&gt;

&lt;p&gt;The pipeline reads from Redpanda using the &lt;code&gt;kafka_franz&lt;/code&gt; input component. Because Redpanda is Kafka API-compatible, this component works without any modification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka_franz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seed_brokers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${REDPANDA_BROKERS:localhost:9092}"&lt;/span&gt;
    &lt;span class="na"&gt;topics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;application-errors&lt;/span&gt;
    &lt;span class="na"&gt;consumer_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jira-error-processor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;seed_brokers&lt;/code&gt; field accepts a list of broker addresses. The &lt;code&gt;${REDPANDA_BROKERS:localhost:9092}&lt;/code&gt; syntax reads from an environment variable and falls back to &lt;code&gt;localhost:9092&lt;/code&gt; if the variable is not set. For Redpanda Serverless or a remote cluster, set &lt;code&gt;REDPANDA_BROKERS&lt;/code&gt; to your bootstrap URL. The &lt;code&gt;consumer_group&lt;/code&gt; gives this consumer its own offset tracking, so the pipeline picks up where it left off after a restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering for Critical Events
&lt;/h3&gt;

&lt;p&gt;The first &lt;code&gt;mapping&lt;/code&gt; processor filters out events that are not critical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;root = if this.severity == "critical" {&lt;/span&gt;
          &lt;span class="s"&gt;this&lt;/span&gt;
        &lt;span class="s"&gt;} else {&lt;/span&gt;
          &lt;span class="s"&gt;deleted()&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;deleted()&lt;/code&gt; in a Bloblang mapping drops the message from the pipeline entirely. Events with &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;info&lt;/code&gt; severity never reach the next processor. This keeps your Jira project clean and avoids unnecessary API calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shaping the Jira API Payload
&lt;/h3&gt;

&lt;p&gt;The second &lt;code&gt;mapping&lt;/code&gt; processor transforms the error event into the JSON structure Jira's REST API expects for creating an issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.project.key = "OPS"&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.summary = "CRITICAL: " + this.service + " - " + this.message&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.description = "Service: " + this.service + "\nMessage: " + this.message + "\nTimestamp: " + this.timestamp + "\nSeverity: " + this.severity&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.issuetype.name = "Bug"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;"OPS"&lt;/code&gt; with your actual Jira project key. The mapping builds a nested JSON object with &lt;code&gt;fields.project.key&lt;/code&gt;, &lt;code&gt;fields.summary&lt;/code&gt;, &lt;code&gt;fields.description&lt;/code&gt;, and &lt;code&gt;fields.issuetype.name&lt;/code&gt;, which matches Jira's v2 REST API schema for issue creation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calling the Jira REST API
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;http&lt;/code&gt; processor sends the shaped payload to Jira:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JIRA_BASE_URL}/rest/api/2/issue"&lt;/span&gt;
        &lt;span class="na"&gt;verb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
        &lt;span class="na"&gt;basic_auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JIRA_USERNAME}"&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JIRA_API_TOKEN}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processor takes the current message content (the Jira payload shaped in the previous step) and sends it as the POST request body. Jira Cloud authenticates API requests using Basic Auth where the username is your Atlassian account email and the password is the API token you generated in the prerequisites. The processor replaces the message content with Jira's API response, which contains the new issue key (e.g. &lt;code&gt;OPS-42&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Store all credentials in environment variables. Never hardcode tokens in YAML files.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Complete Pipeline
&lt;/h3&gt;

&lt;p&gt;Here is the full &lt;code&gt;connect.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka_franz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seed_brokers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${REDPANDA_BROKERS:localhost:9092}"&lt;/span&gt;
    &lt;span class="na"&gt;topics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;application-errors&lt;/span&gt;
    &lt;span class="na"&gt;consumer_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jira-error-processor&lt;/span&gt;

&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;root = if this.severity == "critical" {&lt;/span&gt;
          &lt;span class="s"&gt;this&lt;/span&gt;
        &lt;span class="s"&gt;} else {&lt;/span&gt;
          &lt;span class="s"&gt;deleted()&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.project.key = "OPS"&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.summary = "CRITICAL: " + this.service + " - " + this.message&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.description = "Service: " + this.service + "\nMessage: " + this.message + "\nTimestamp: " + this.timestamp + "\nSeverity: " + this.severity&lt;/span&gt;
        &lt;span class="s"&gt;root.fields.issuetype.name = "Bug"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JIRA_BASE_URL}/rest/api/2/issue"&lt;/span&gt;
        &lt;span class="na"&gt;verb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
        &lt;span class="na"&gt;basic_auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JIRA_USERNAME}"&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${JIRA_API_TOKEN}"&lt;/span&gt;

&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;codec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lines&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;stdout&lt;/code&gt; output prints the Jira API response for each created issue. In production you would route this to a Redpanda topic or a logging aggregator instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Producing Test Events
&lt;/h3&gt;

&lt;p&gt;The companion repository includes a Python producer at &lt;code&gt;producer/produce_errors.py&lt;/code&gt; that publishes mock error events. Clone the repository and install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/draftdev/test--how-to-use-redpanda-connects-jira-processor
&lt;span class="nb"&gt;cd &lt;/span&gt;test--how-to-use-redpanda-connects-jira-processor
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer sends three events to the &lt;code&gt;application-errors&lt;/code&gt; topic: two with &lt;code&gt;critical&lt;/code&gt; severity and one with &lt;code&gt;warning&lt;/code&gt; severity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;confluent_kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Producer&lt;/span&gt;

&lt;span class="n"&gt;TOPIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application-errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BROKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDPANDA_BROKERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:9092&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connection pool exhausted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth-service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Elevated token refresh rate detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Database write timeout after 30s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Producer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bootstrap.servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BROKERS&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOPIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sent: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Done.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline should create two Jira issues and discard the &lt;code&gt;warning&lt;/code&gt; event, giving you a clear signal that the filter is working correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the Pipeline and Verifying Output
&lt;/h3&gt;

&lt;p&gt;Create the topic if it does not already exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk topic create application-errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Export your environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REDPANDA_BROKERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"localhost:9092"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JIRA_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://your-org.atlassian.net"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JIRA_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-email@example.com"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JIRA_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-api-token-here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk connect run connect.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect prints its startup logs, confirms the consumer group is active, and starts waiting for messages. Keep this terminal open.&lt;/p&gt;

&lt;p&gt;In a second terminal, run the producer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python producer/produce_errors.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see three lines in the producer terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sent: payment-api [critical]
Sent: auth-service [warning]
Sent: order-service [critical]
Done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back in the pipeline terminal, the Jira API responses appear for the two critical events. Each response contains the new issue key and ID. The &lt;code&gt;warning&lt;/code&gt; event produces no output because &lt;code&gt;deleted()&lt;/code&gt; removes it before it reaches the &lt;code&gt;http&lt;/code&gt; processor.&lt;/p&gt;

&lt;p&gt;To confirm the raw data that came off the topic independently of the pipeline, use &lt;code&gt;rpk topic consume&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk topic consume application-errors &lt;span class="nt"&gt;--num&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three events print from the topic, confirming your producer and Redpanda are healthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the Jira Issues
&lt;/h3&gt;

&lt;p&gt;Navigate to your Jira project and check the issue list. You should see two new &lt;code&gt;Bug&lt;/code&gt; issues: one for the &lt;code&gt;payment-api&lt;/code&gt; connection pool error and one for the &lt;code&gt;order-service&lt;/code&gt; database write timeout.&lt;/p&gt;

&lt;p&gt;[SCREENSHOT: Jira project showing two automatically created Bug issues with summaries matching "CRITICAL: payment-api" and "CRITICAL: order-service"]&lt;/p&gt;

&lt;p&gt;If the issues don't appear, I'd check the pipeline terminal first for error responses from the Jira API. Some common causes are a wrong &lt;code&gt;base_url&lt;/code&gt; format (it should not include a trailing slash), an expired API token, or a project key that does not exist in your Jira instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Build Next
&lt;/h2&gt;

&lt;p&gt;I've shown you how to build a pipeline that consumes a Redpanda topic, filters events by severity, shapes each event into a Jira API payload, and creates the ticket automatically. No webhook receiver, no custom integration script, no scheduled job watching a dashboard. The whole thing lives in one YAML file, so the alerting logic sits right alongside the rest of your pipeline config instead of being a separate service to maintain. &lt;/p&gt;

&lt;p&gt;The pattern extends further. You can add a &lt;code&gt;switch&lt;/code&gt; processor to route critical errors from different services to different Jira projects based on the &lt;code&gt;service&lt;/code&gt; field. You can also adjust the &lt;code&gt;mapping&lt;/code&gt; to set priority, add labels, or assign the issue to a specific team member. For deduplication, add a cache or database lookup before the &lt;code&gt;http&lt;/code&gt; processor to check whether an open issue already exists for the same service and error type.&lt;/p&gt;

</description>
      <category>redpanda</category>
      <category>eventdriven</category>
      <category>redpandaconnect</category>
      <category>jira</category>
    </item>
    <item>
      <title>Building a Custom Processor Plugin for Redpanda Connect</title>
      <dc:creator>Aykut Bulgu</dc:creator>
      <pubDate>Thu, 06 Aug 2026 21:21:38 +0000</pubDate>
      <link>https://dev.to/mabulgu/building-a-custom-processor-plugin-for-redpanda-connect-1ljl</link>
      <guid>https://dev.to/mabulgu/building-a-custom-processor-plugin-for-redpanda-connect-1ljl</guid>
      <description>&lt;p&gt;&lt;a href="https://docs.redpanda.com/connect/home/" rel="noopener noreferrer"&gt;Redpanda Connect&lt;/a&gt; ships with a wide range of connectors and processors for building data pipelines. But, in real-world pipelines, sometimes you need transformation logic that’s not already built-in. I wanted to see how far you could take that without leaving Python, so this walks through building a dynamic plugin for Redpanda Connect.&lt;/p&gt;

&lt;p&gt;Dynamic plugins let you extend Redpanda Connect with custom inputs, processors, or outputs written in any language that supports gRPC, running as separate subprocesses alongside the main engine. This gives you language flexibility while still using Redpanda Connect's normal pipeline config and orchestration. Your plugin is just another component in the YAML pipeline config. It loads at startup, processes messages alongside built-in components, and shuts down cleanly when the pipeline stops.&lt;/p&gt;

&lt;p&gt;In this tutorial, I'll show you how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a custom Python processor plugin that transforms messages in a Redpanda Connect pipeline&lt;/li&gt;
&lt;li&gt;Declare a plugin manifest and wire it into a YAML pipeline config&lt;/li&gt;
&lt;li&gt;Run and verify the plugin end to end using &lt;code&gt;rpk connect run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extend the plugin to accept runtime configuration through the manifest&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Use a Dynamic Plugin
&lt;/h2&gt;

&lt;p&gt;Suppose most of your pipeline uses built-in Redpanda Connect processors for filtering and routing, but one step needs a proprietary text normalization routine written in Python. That routine changes often, and you don't want to recompile a Go binary every time the logic updates.&lt;/p&gt;

&lt;p&gt;Dynamic plugins solve exactly this. You write the normalization logic as a Python function, package it alongside a plugin manifest, and load it into the pipeline at runtime with the &lt;code&gt;--rpc-plugins&lt;/code&gt; flag. The plugin runs as a separate subprocess communicating with the main Redpanda Connect engine over gRPC. No Go code, no recompilation, no changes to the core binary.&lt;/p&gt;

&lt;p&gt;Here's a high-level architecture diagram of a pipeline with a dynamic plugin:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwf26y10c01j4qeb8rqke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwf26y10c01j4qeb8rqke.png" alt="Architecture diagram" width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Splitting the plugin into its own process has a few practical benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the plugin crashes, it doesn't take the main engine down with it. Redpanda Connect notices and restarts the subprocess.&lt;/li&gt;
&lt;li&gt;You're not locked into Go. Any language with gRPC libraries works, and the official Python SDK handles the protocol so you're only writing the actual transformation logic.&lt;/li&gt;
&lt;li&gt;The plugin is its own subprocess, so you can develop, test, and deploy it separately from the rest of the pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You'll need these installed to follow along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.redpanda.com/redpanda-connect/home/" rel="noopener noreferrer"&gt;Redpanda Connect&lt;/a&gt; (&lt;code&gt;rpk&lt;/code&gt; CLI, v4.56.0 or later, which includes &lt;code&gt;rpk connect&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Python 3.12 or later&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; (Python package manager)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basic familiarity with YAML pipeline configuration is helpful but not required. I'll explain each configuration field as it comes up throughout the tutorial.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up the Plugin Project
&lt;/h3&gt;

&lt;p&gt;Create the plugin directory and initialize a Python project inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; plugins/yell-processor
&lt;span class="nb"&gt;cd &lt;/span&gt;plugins/yell-processor
uv init &lt;span class="nt"&gt;--no-readme&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;uv init&lt;/code&gt; command creates a &lt;code&gt;pyproject.toml&lt;/code&gt; and sets up a virtual environment. The &lt;code&gt;--no-readme&lt;/code&gt; flag skips generating a README file since the plugin directory only needs the processor script and the manifest.&lt;/p&gt;

&lt;p&gt;Install the Redpanda Connect Python SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add redpanda_connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adds the &lt;code&gt;redpanda_connect&lt;/code&gt; package to the project and installs it in the virtual environment. The SDK provides the decorator, message types, and gRPC server that your plugin needs to communicate with Redpanda Connect. Return to the project root before continuing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project directory should have this structure at this point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── plugins/
│   └── yell-processor/
│       ├── pyproject.toml
│       └── .venv/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Writing the Processor Logic
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;plugins/yell-processor/yell_processor.py&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redpanda_connect&lt;/span&gt;


&lt;span class="nd"&gt;@redpanda_connect.processor&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;yell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;redpanda_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;redpanda_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redpanda_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processor_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yell&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@redpanda_connect.processor&lt;/code&gt; decorator marks the &lt;code&gt;yell&lt;/code&gt; function as a processor component. The function receives a &lt;code&gt;Message&lt;/code&gt; object and returns a modified &lt;code&gt;Message&lt;/code&gt;. The SDK's &lt;code&gt;Message&lt;/code&gt; is a dataclass with two main fields: &lt;code&gt;payload&lt;/code&gt; holds the message content (bytes or a structured value), and &lt;code&gt;metadata&lt;/code&gt; is a dictionary of key-value pairs for additional context outside the payload.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;processor_main()&lt;/code&gt; call at the bottom starts a gRPC server that listens on a Unix socket. Redpanda Connect launches this script as a subprocess and communicates with it through that socket. You do not need to manage the connection or protocol yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declaring the Plugin Manifest
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;plugins/yell-processor/plugin.yaml&lt;/code&gt; alongside the processor script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yell&lt;/span&gt;
&lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Converts every message payload to uppercase.&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uv"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yell_processor.py"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;processor&lt;/span&gt;
&lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field in the manifest serves a specific purpose. &lt;code&gt;name&lt;/code&gt; is the identifier you reference in &lt;code&gt;connect.yaml&lt;/code&gt; when wiring the plugin into a pipeline. &lt;code&gt;summary&lt;/code&gt; is a human-readable description of what the plugin does. &lt;code&gt;command&lt;/code&gt; is the shell command array that Redpanda Connect executes to start the plugin subprocess, and using &lt;code&gt;uv run&lt;/code&gt; ensures the script runs inside the project's virtual environment with all dependencies available. &lt;code&gt;type&lt;/code&gt; declares the component type, accepting &lt;code&gt;processor&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, or &lt;code&gt;output&lt;/code&gt;. &lt;code&gt;fields&lt;/code&gt; lists configurable parameters that users can pass from the pipeline config, and an empty array means the plugin has no configurable fields.&lt;/p&gt;

&lt;p&gt;If a plugin fails to load, the first thing I'd check is whether there’s a mismatch between the &lt;code&gt;command&lt;/code&gt; path and the actual script filename. Make sure &lt;code&gt;yell_processor.py&lt;/code&gt; exists in the same directory as &lt;code&gt;plugin.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wiring the Plugin into a Pipeline
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;connect.yaml&lt;/code&gt; in the project root (one level above &lt;code&gt;plugins/&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;let events = ["user signed up from mobile-app", "payment processed for order 1042", "dashboard export requested", "session timeout on web-client", "inventory sync completed"]&lt;/span&gt;
      &lt;span class="s"&gt;root = $events.index(counter() % 5)&lt;/span&gt;

&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;yell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;codec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lines&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;generate&lt;/code&gt; input produces five test messages at one-second intervals, cycling through a list of event log strings that simulate upstream service traffic. The &lt;code&gt;yell: {}&lt;/code&gt; entry under &lt;code&gt;processors&lt;/code&gt; tells Connect to route each message through the plugin registered under the name &lt;code&gt;yell&lt;/code&gt;. The empty braces pass no configuration to the plugin, which works because this version has no configurable fields. The &lt;code&gt;stdout&lt;/code&gt; output prints each processed message to the terminal.&lt;/p&gt;

&lt;p&gt;In a production pipeline, you would replace &lt;code&gt;generate&lt;/code&gt; and &lt;code&gt;stdout&lt;/code&gt; with actual data sources and targets such as Redpanda topics, HTTP endpoints, or database connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running and Verifying the Pipeline
&lt;/h3&gt;

&lt;p&gt;Start the pipeline by pointing &lt;code&gt;--rpc-plugins&lt;/code&gt; at the plugin manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk connect run &lt;span class="nt"&gt;--rpc-plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;plugins/yell-processor/plugin.yaml connect.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect reads the manifest, launches &lt;code&gt;uv run yell_processor.py&lt;/code&gt; as a subprocess, establishes a gRPC connection, and registers the plugin under the name &lt;code&gt;yell&lt;/code&gt;. The terminal output includes startup logs followed by the processed messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;INFO Running main config from specified file    path=connect.yaml
INFO Listening for HTTP requests at: http://0.0.0.0:4195
INFO Launching a Redpanda Connect instance, use CTRL+C to close
INFO Output type stdout is now active
INFO Input type generate is now active
USER SIGNED UP FROM MOBILE-APP
PAYMENT PROCESSED FOR ORDER 1042
DASHBOARD EXPORT REQUESTED
SESSION TIMEOUT ON WEB-CLIENT
INVENTORY SYNC COMPLETED
INFO Pipeline has terminated. Shutting down the service
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each uppercase line confirms that the plugin received an event message, converted the payload to uppercase, and returned it to the pipeline. The pipeline exits automatically after five messages because of the &lt;code&gt;count: 5&lt;/code&gt; setting in the input config.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the Plugin Configurable
&lt;/h3&gt;

&lt;p&gt;The basic yell processor has its behavior hardcoded. The plugin system supports runtime configuration through the &lt;code&gt;fields&lt;/code&gt; array in &lt;code&gt;plugin.yaml&lt;/code&gt; and the config dictionary passed to the constructor function.&lt;/p&gt;

&lt;p&gt;To accept configuration, you replace the &lt;code&gt;@redpanda_connect.processor&lt;/code&gt; decorator with a manual constructor pattern. The constructor is a function named &lt;code&gt;processor&lt;/code&gt; that receives the config as a dictionary and returns an object with &lt;code&gt;process()&lt;/code&gt; and &lt;code&gt;close()&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;plugins/yell-processor/yell_processor.py&lt;/code&gt; to this configurable version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redpanda_connect&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;YellProcessor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repeat_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repeat_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repeat_count&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;redpanda_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageBatch&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;redpanda_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageBatch&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repeat_count&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;redpanda_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;YellProcessor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;repeat_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="n"&gt;repeat_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;YellProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repeat_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redpanda_connect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processor_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;processor()&lt;/code&gt; function at the bottom replaces the decorator. Connect calls this function once at startup and passes any configuration values as a &lt;code&gt;Value&lt;/code&gt; dictionary. The function reads &lt;code&gt;prefix&lt;/code&gt; and &lt;code&gt;repeat_count&lt;/code&gt; from the config with safe defaults, then returns a &lt;code&gt;YellProcessor&lt;/code&gt; instance. The &lt;code&gt;process()&lt;/code&gt; method operates on message batches rather than individual messages, which is the interface the dynamic plugin system uses internally to spread serialization costs across multiple messages.&lt;/p&gt;

&lt;p&gt;Update &lt;code&gt;plugin.yaml&lt;/code&gt; to declare the new fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yell&lt;/span&gt;
&lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Converts every message payload to uppercase with optional prefix and repeat.&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uv"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yell_processor.py"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;processor&lt;/span&gt;
&lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prefix&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repeat_count&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass values for these fields in &lt;code&gt;connect.yaml&lt;/code&gt; by replacing the empty braces with a mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;yell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="na"&gt;repeat_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the pipeline again with the same command. The output now includes the prefix and repeats each transformed message twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt; USER SIGNED UP FROM MOBILE-APP&amp;gt;&amp;gt; USER SIGNED UP FROM MOBILE-APP
&amp;gt;&amp;gt; PAYMENT PROCESSED FOR ORDER 1042&amp;gt;&amp;gt; PAYMENT PROCESSED FOR ORDER 1042
&amp;gt;&amp;gt; DASHBOARD EXPORT REQUESTED&amp;gt;&amp;gt; DASHBOARD EXPORT REQUESTED
&amp;gt;&amp;gt; SESSION TIMEOUT ON WEB-CLIENT&amp;gt;&amp;gt; SESSION TIMEOUT ON WEB-CLIENT
&amp;gt;&amp;gt; INVENTORY SYNC COMPLETED&amp;gt;&amp;gt; INVENTORY SYNC COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, I walked you through building a custom Python processor plugin that runs inside a Redpanda Connect pipeline with full process isolation and no Go code required. I started with a minimal decorator-based processor, declared a plugin manifest, wired it into a pipeline, and then extended it to accept runtime configuration through the manifest fields.&lt;/p&gt;

&lt;p&gt;The same pattern applies to the other two component types: inputs that pull data from external systems and outputs that push data to external targets. You can apply this approach to build custom inputs that pull from proprietary APIs, processors that call internal ML models or normalize data formats, or outputs that write to systems Redpanda Connect does not natively support. Each plugin is a standalone subprocess that you can package, version, and distribute independently. That process boundary is really the whole point, because it allows you to keep iterating in Python without ever touching the core pipeline.&lt;/p&gt;

&lt;p&gt;The complete companion code for this tutorial is available at &lt;a href="https://github.com/SystemCraftsman/redpanda-connect-dynamic-plugin-demo" rel="noopener noreferrer"&gt;github.com/SystemCraftsman/redpanda-connect-dynamic-plugin-demo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>redpanda</category>
      <category>kafkaconnect</category>
      <category>redpandaconnect</category>
    </item>
    <item>
      <title>How to Configure Tracer Components in Redpanda Connect for Distributed Tracing</title>
      <dc:creator>Aykut Bulgu</dc:creator>
      <pubDate>Wed, 29 Jul 2026 21:34:01 +0000</pubDate>
      <link>https://dev.to/mabulgu/how-to-configure-tracer-components-in-redpanda-connect-for-distributed-tracing-k1p</link>
      <guid>https://dev.to/mabulgu/how-to-configure-tracer-components-in-redpanda-connect-for-distributed-tracing-k1p</guid>
      <description>&lt;p&gt;Distributed data pipelines have become a core building block of modern event-driven architectures. As these pipelines grow in complexity with multiple processors filtering, transforming, and routing messages, debugging them becomes a real challenge. When a message goes missing or a processor behaves unexpectedly, you need visibility into exactly where things broke down and how long each step took.&lt;/p&gt;

&lt;p&gt;Distributed tracing solves this problem. Instead of relying solely on logs that tell you something happened, traces show you the full journey of every message through your pipeline. I'm going to show you how to implement that kind of tracing using &lt;a href="https://docs.redpanda.com/redpanda-connect/components/tracers/about/" rel="noopener noreferrer"&gt;Redpanda Connect's &lt;code&gt;tracer&lt;/code&gt; component&lt;/a&gt;, which writes &lt;a href="https://opentelemetry.io/docs/what-is-opentelemetry/" rel="noopener noreferrer"&gt;OpenTelemetry-compatible&lt;/a&gt; trace data directly to a Redpanda topic, so you can consume trace data the same way you consume any other event stream.&lt;/p&gt;

&lt;p&gt;Here's what I'll walk you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configure the Redpanda Connect tracer component with a Jaeger backend&lt;/li&gt;
&lt;li&gt;Set up the OpenTelemetry Collector tracer for a backend-agnostic approach&lt;/li&gt;
&lt;li&gt;Build a multi-step pipeline that ingests, filters, transforms, and routes application events&lt;/li&gt;
&lt;li&gt;Explore sampling strategies for controlling trace volume in production&lt;/li&gt;
&lt;li&gt;Visualize trace spans in the Jaeger UI to identify bottlenecks in your pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring Tracer Components in Redpanda Connect for Distributed Tracing
&lt;/h2&gt;

&lt;p&gt;Suppose that you work for an online payment processing company that handles thousands of transaction events per second. These events come in different types such as &lt;code&gt;purchase&lt;/code&gt;, &lt;code&gt;refund&lt;/code&gt;, and &lt;code&gt;chargeback&lt;/code&gt;. You've got a Redpanda Connect pipeline that ingests these events from a Redpanda input topic, filters out invalid transactions, enriches each event with additional metadata, and routes them to different output topics based on event type.&lt;/p&gt;

&lt;p&gt;The pipeline works well, but you keep running into the same problem: when a transaction takes longer than expected or silently fails to reach its output topic, there's no easy way to pinpoint which processor caused the delay or failure. Multi-step pipelines with conditional logic are exactly where bottlenecks and silent failures hide.&lt;/p&gt;

&lt;p&gt;The following diagram shows the high-level architecture of the pipeline with tracing enabled:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fac2xoz7hp9wx7pvpen27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fac2xoz7hp9wx7pvpen27.png" alt="High-level architecture of the pipeline" width="800" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By enabling the Redpanda tracer component, every transaction event that flows through the pipeline produces a trace. Each processor in the chain generates a child span, a nested timing record showing how long that step took and whether it succeeded. That lets you see exactly how long filtering, transformation, and routing take for each event. If a &lt;code&gt;refund&lt;/code&gt; event takes three times longer than a &lt;code&gt;purchase&lt;/code&gt; event, the trace data will show exactly which processor is responsible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You'll need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/get-started/get-docker/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; and &lt;a href="https://docs.docker.com/compose/install/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt; (Docker Engine 29 or higher)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.redpanda.com/redpanda-connect/get-started/quickstarts/" rel="noopener noreferrer"&gt;Redpanda Connect 4.88.0 or higher&lt;/a&gt; installed via &lt;code&gt;rpk connect install&lt;/code&gt; or &lt;code&gt;brew install redpanda-data/tap/redpanda&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.redpanda.com/current/get-started/rpk-install/" rel="noopener noreferrer"&gt;rpk CLI&lt;/a&gt; for managing Redpanda topics and consuming trace data&lt;/li&gt;
&lt;li&gt;Basic familiarity with &lt;a href="https://docs.redpanda.com/redpanda-connect/configuration/about/" rel="noopener noreferrer"&gt;Redpanda Connect's YAML pipeline configuration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.jaegertracing.io/download/" rel="noopener noreferrer"&gt;Jaeger 2.17.0 or higher&lt;/a&gt; (runs via Docker Compose in this tutorial)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How the Redpanda Connect Tracer Component Works
&lt;/h3&gt;

&lt;p&gt;Every Redpanda Connect pipeline config supports an optional top-level &lt;code&gt;tracer&lt;/code&gt; block. When that block is present and pointing at a live backend, the runtime allocates a root span for each message the moment it enters the input. As the message flows through your processor chain, each processor appends a child span to that root. The child span captures the processor type, its execution duration, and any error status if the processor fails.&lt;/p&gt;

&lt;p&gt;A single message trace produces a nested span hierarchy flowing from input through each processor to output:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwajmvhykbu9c72bbs05j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwajmvhykbu9c72bbs05j.png" alt="Trace hierarchy" width="799" height="63"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because &lt;a href="https://docs.redpanda.com/redpanda-connect/components/tracers/open_telemetry_collector/" rel="noopener noreferrer"&gt;trace context propagates through Kafka message headers&lt;/a&gt;, a Redpanda Connect pipeline sitting in the middle of a larger distributed system can both receive an upstream trace context and pass it downstream. You get end-to-end traces across multiple services, extending well beyond the pipeline boundary.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tracer&lt;/code&gt; block is a sibling to &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;pipeline&lt;/code&gt;, and &lt;code&gt;output&lt;/code&gt; in your config file. Adding it activates trace emission without changing any pipeline behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up the Demo Pipeline
&lt;/h3&gt;

&lt;p&gt;I'm using a synthetic data pipeline for these examples: a &lt;code&gt;generate&lt;/code&gt; input emitting JSON events, a &lt;a href="https://docs.redpanda.com/redpanda-connect/components/processors/bloblang/" rel="noopener noreferrer"&gt;&lt;code&gt;bloblang&lt;/code&gt; processor&lt;/a&gt; enriching each event, a &lt;code&gt;log&lt;/code&gt; processor confirming the transformation, and a &lt;code&gt;stdout&lt;/code&gt; output. This setup makes it easy to observe trace structure without any external Kafka dependency.&lt;/p&gt;

&lt;p&gt;Run the following command to create a workspace directory and navigate into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/redpanda-tracer-tutorial &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/redpanda-tracer-tutorial
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file named &lt;code&gt;01-basic-pipeline.yaml&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;root.user_id = uuid_v4()&lt;/span&gt;
      &lt;span class="s"&gt;root.event = "purchase"&lt;/span&gt;
      &lt;span class="s"&gt;root.amount = random_int(min: 1, max: 500)&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;

&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;bloblang&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;root = this&lt;/span&gt;
        &lt;span class="s"&gt;root.currency = "USD"&lt;/span&gt;
        &lt;span class="s"&gt;root.processed_at = now()&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${!json(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;)}"&lt;/span&gt;

&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;generate&lt;/code&gt; input creates 20 synthetic events at one-second intervals, each with a &lt;code&gt;user_id&lt;/code&gt;, an &lt;code&gt;event&lt;/code&gt; type, and a random &lt;code&gt;amount&lt;/code&gt;. The &lt;code&gt;bloblang&lt;/code&gt; processor enriches each event by adding &lt;code&gt;currency&lt;/code&gt; and &lt;code&gt;processed_at&lt;/code&gt; fields, and the &lt;code&gt;log&lt;/code&gt; processor prints a confirmation message for each processed event.&lt;/p&gt;

&lt;p&gt;Run the following command to confirm the pipeline works before adding tracing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk connect run 01-basic-pipeline.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see 20 JSON objects emitted to stdout. Once the base pipeline is confirmed, you're ready to add the tracer block.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the Jaeger Tracer in Redpanda Connect
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.jaegertracing.io/" rel="noopener noreferrer"&gt;Jaeger&lt;/a&gt; remains one of the most common local tracing backends for development. Redpanda Connect ships a native &lt;a href="https://docs.redpanda.com/redpanda-connect/components/tracers/jaeger/" rel="noopener noreferrer"&gt;&lt;code&gt;jaeger&lt;/code&gt; tracer type&lt;/a&gt; that communicates with a Jaeger agent over UDP (port 6831 by default) or with a Jaeger collector over HTTP.&lt;/p&gt;

&lt;p&gt;Download the following &lt;a href="https://raw.githubusercontent.com/SystemCraftsman/redpanda-connect-configuring-tracer-components-demo/main/docker/docker-compose.yml" rel="noopener noreferrer"&gt;docker-compose.yml&lt;/a&gt; file to start a local Jaeger all-in-one instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://raw.githubusercontent.com/SystemCraftsman/redpanda-connect-configuring-tracer-components-demo/main/docker/docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the following command to start Jaeger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up jaeger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one" rel="noopener noreferrer"&gt;all-in-one image&lt;/a&gt; exposes the Jaeger UI at &lt;code&gt;http://localhost:16686&lt;/code&gt;, accepts HTTP collector traffic on port &lt;code&gt;14268&lt;/code&gt;, and accepts UDP agent traffic on port &lt;code&gt;6831&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Copy &lt;code&gt;01-basic-pipeline.yaml&lt;/code&gt; to a new file named &lt;code&gt;02-jaeger-tracing.yaml&lt;/code&gt; and add the following &lt;code&gt;tracer&lt;/code&gt; block at the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jaeger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;collector_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:14268/api/traces&lt;/span&gt;
    &lt;span class="na"&gt;sampler_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;const&lt;/span&gt;
    &lt;span class="na"&gt;sampler_param&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;purchase-events&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;collector_url&lt;/code&gt; field sends spans via HTTP to the Jaeger collector endpoint. This is the recommended approach for local Docker setups because the UDP-based &lt;code&gt;agent_address&lt;/code&gt; option can be unreliable on the macOS Docker Desktop. &lt;code&gt;sampler_type: const&lt;/code&gt; with &lt;code&gt;sampler_param: 1&lt;/code&gt; traces every message, which is appropriate for local development where you want complete visibility. The &lt;code&gt;tags&lt;/code&gt; map attaches arbitrary key-value pairs to every span from this pipeline, which helps when you're running multiple pipelines and filtering traces in the Jaeger UI by service or environment.&lt;/p&gt;

&lt;p&gt;Run the following command to start the traced pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk connect run 02-jaeger-tracing.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the 20 messages process, open &lt;code&gt;http://localhost:16686&lt;/code&gt;, select the service named &lt;code&gt;benthos&lt;/code&gt; (the default service name for Redpanda Connect pipelines), and click &lt;em&gt;Find Traces&lt;/em&gt;. Each trace represents a single generated message. Click into any trace to see the root span covering the full pipeline execution time, with child spans for the &lt;code&gt;bloblang&lt;/code&gt; processor and the &lt;code&gt;log&lt;/code&gt; processor nested underneath.&lt;/p&gt;

&lt;p&gt;To verify that traces are being collected, you should see 20 traces listed in the Jaeger UI. Each trace must contain a root span and two child spans corresponding to the &lt;code&gt;bloblang&lt;/code&gt; and &lt;code&gt;log&lt;/code&gt; processors.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;agent_address&lt;/code&gt; field is an alternative that sends spans via UDP to a Jaeger agent (requires port &lt;code&gt;6831&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jaeger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;agent_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:6831&lt;/span&gt;
    &lt;span class="na"&gt;sampler_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;const&lt;/span&gt;
    &lt;span class="na"&gt;sampler_param&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;agent_address&lt;/code&gt; in environments where a local Jaeger agent sidecar is already running and UDP connectivity is reliable. In Kubernetes environments, most teams prefer &lt;code&gt;collector_url&lt;/code&gt; pointing at a centralized collector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the OpenTelemetry Collector Tracer
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.redpanda.com/redpanda-connect/components/tracers/open_telemetry_collector/" rel="noopener noreferrer"&gt;&lt;code&gt;open_telemetry_collector&lt;/code&gt; tracer type&lt;/a&gt; sends spans via the &lt;a href="https://opentelemetry.io/docs/specs/otlp/" rel="noopener noreferrer"&gt;OpenTelemetry Protocol (OTLP)&lt;/a&gt; over gRPC or HTTP. This approach is backend-agnostic, pointing Redpanda Connect at an &lt;a href="https://opentelemetry.io/docs/collector/" rel="noopener noreferrer"&gt;OTel Collector&lt;/a&gt; that routes spans to Jaeger, Grafana Tempo, Honeycomb, Datadog, or any other OTLP-compatible destination.&lt;/p&gt;

&lt;p&gt;Download the following &lt;a href="https://raw.githubusercontent.com/SystemCraftsman/redpanda-connect-configuring-tracer-components-demo/main/docker/otel-collector-config.yaml" rel="noopener noreferrer"&gt;otel-collector-config.yaml&lt;/a&gt; file. It accepts OTLP traffic on port 4317 (gRPC) and port 4318 (HTTP), applies a batch processor, and exports spans to the local Jaeger instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://raw.githubusercontent.com/SystemCraftsman/redpanda-connect-configuring-tracer-components-demo/main/docker/otel-collector-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downloaded file should have the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4317&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4318&lt;/span&gt;

&lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp_grpc/jaeger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jaeger:4317&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp_grpc/jaeger&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the following command to start both Jaeger and the OTel Collector together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy &lt;code&gt;01-basic-pipeline.yaml&lt;/code&gt; to a new file named &lt;code&gt;03-otel-tracing.yaml&lt;/code&gt; and add the following &lt;code&gt;tracer&lt;/code&gt; block at the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;open_telemetry_collector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;purchase-pipeline&lt;/span&gt;
    &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:4317&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;sampling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;service&lt;/code&gt; field sets the service name that appears in your tracing UI. Changing it from the default &lt;code&gt;benthos&lt;/code&gt; to something descriptive (like &lt;code&gt;purchase-pipeline&lt;/code&gt;) makes it much easier to locate spans when you have multiple pipelines sending to the same collector.&lt;/p&gt;

&lt;p&gt;Note that the &lt;code&gt;grpc&lt;/code&gt; list accepts multiple endpoints, which is useful when your collector runs behind a load balancer or when you need redundancy. Each entry takes an &lt;code&gt;address&lt;/code&gt; and an optional &lt;code&gt;secure&lt;/code&gt; field for TLS.&lt;/p&gt;

&lt;p&gt;Run the following command to start the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk connect run 03-otel-tracing.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify, open the Jaeger UI at &lt;code&gt;http://localhost:16686&lt;/code&gt; and search for the &lt;code&gt;purchase-pipeline&lt;/code&gt; service. The span structure is identical to what you saw with the native Jaeger tracer: a root span per message with processor child spans nested underneath. You should see the traces listed under the &lt;code&gt;purchase-pipeline&lt;/code&gt; service name instead of the default &lt;code&gt;benthos&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The HTTP transport option works the same way, using &lt;a href="https://opentelemetry.io/docs/specs/otlp/#otlphttp-default-port" rel="noopener noreferrer"&gt;port 4318 by OTLP convention&lt;/a&gt;. Make sure port 4318 is exposed in your &lt;code&gt;docker-compose.yml&lt;/code&gt; (the compose file used in this tutorial already includes it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;open_telemetry_collector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;purchase-pipeline&lt;/span&gt;
    &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:4318&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the address value does not include a protocol prefix. Redpanda Connect handles the HTTP transport internally based on the &lt;code&gt;http&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;Use HTTP when your environment restricts gRPC traffic or when your OTel Collector only exposes an HTTP endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing a Sampling Strategy for Redpanda Connect Traces
&lt;/h3&gt;

&lt;p&gt;Sampling controls what fraction of traces gets exported. At high message volumes, exporting every span is expensive in both network overhead and backend storage. The &lt;a href="https://opentelemetry.io/docs/concepts/sampling/" rel="noopener noreferrer"&gt;OpenTelemetry sampling documentation&lt;/a&gt; covers the tradeoffs in depth.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jaeger&lt;/code&gt; tracer supports two sampler types you'll use most often. With &lt;code&gt;sampler_type: const&lt;/code&gt; and &lt;code&gt;sampler_param: 1&lt;/code&gt;, every message produces a trace. Setting &lt;code&gt;sampler_param: 0&lt;/code&gt; disables tracing completely without removing the tracer block, which is useful when you want to keep the config ready but temporarily stop generating spans.&lt;/p&gt;

&lt;p&gt;For probabilistic sampling, set &lt;code&gt;sampler_type: probabilistic&lt;/code&gt; and give it a ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jaeger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;collector_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:14268/api/traces&lt;/span&gt;
    &lt;span class="na"&gt;sampler_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;probabilistic&lt;/span&gt;
    &lt;span class="na"&gt;sampler_param&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sampler_param: 0.1&lt;/code&gt; samples approximately 10% of messages. That's roughly where I'd start for production pipelines doing more than 10,000 messages per second, but you can always tighten it once you see real trace volume.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;open_telemetry_collector&lt;/code&gt; tracer has its own sampling block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;open_telemetry_collector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;purchase-pipeline&lt;/span&gt;
    &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-collector:4317&lt;/span&gt;
    &lt;span class="na"&gt;sampling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;ratio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ratio&lt;/code&gt; field takes a float value between 0 and 1. Wrapping it in quotes causes an unmarshal error because the parser expects a numeric type. Setting &lt;code&gt;enabled: false&lt;/code&gt; (the default) passes all spans through to the collector and lets the collector make sampling decisions, which is the preferred pattern when you want centralized sampling policy control across multiple services via the &lt;a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor" rel="noopener noreferrer"&gt;OTel Collector's tail sampling processor&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Schema Registry for Trace Data
&lt;/h3&gt;

&lt;p&gt;In the previous examples, I used &lt;code&gt;generate&lt;/code&gt; and &lt;code&gt;stdout&lt;/code&gt; with no external dependencies. The Redpanda tracer type, however, writes trace data to a Redpanda topic, so it requires a running Redpanda cluster with broker and schema registry endpoints accessible. You can start a local Redpanda cluster using &lt;a href="https://docs.redpanda.com/current/get-started/quick-start/" rel="noopener noreferrer"&gt;rpk container&lt;/a&gt; or add a Redpanda service to your &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By default, the Redpanda tracer emits trace data in JSON format. While this works well for development, serializing trace data with Redpanda's &lt;a href="https://docs.redpanda.com/current/manage/schema-reg/" rel="noopener noreferrer"&gt;schema registry&lt;/a&gt; matters for downstream consumers that need a consistent and validated schema to parse traces reliably.&lt;/p&gt;

&lt;p&gt;To enable schema registry serialization, set &lt;code&gt;format&lt;/code&gt; to &lt;code&gt;schema-registry-json&lt;/code&gt; and configure the &lt;code&gt;schema_registry.url&lt;/code&gt; in your tracer block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redpanda&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seed_brokers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;localhost:9092&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-traces&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema-registry-json&lt;/span&gt;
    &lt;span class="na"&gt;schema_registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8081&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;purchase-pipeline&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the &lt;code&gt;format&lt;/code&gt; field supports several options: &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;protobuf&lt;/code&gt;, &lt;code&gt;schema-registry-json&lt;/code&gt;, and &lt;code&gt;schema-registry-protobuf&lt;/code&gt;. The &lt;code&gt;schema-registry-json&lt;/code&gt; option registers the trace schema with the schema registry and serializes each trace message accordingly.&lt;/p&gt;

&lt;p&gt;You can verify the deserialized trace output by consuming the &lt;code&gt;otel-traces&lt;/code&gt; topic with &lt;code&gt;rpk&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk topic consume otel-traces &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the following output with trace spans in JSON format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trace_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"span_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bloblang"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"purchase-pipeline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"child_spans"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: While the tracer config API is stable, the format of spans, tags, and logs is subject to change. Avoid building brittle downstream parsers that depend on specific span field names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Adding Compression and Shutdown Delay
&lt;/h3&gt;

&lt;p&gt;For production environments where trace volume can be significant, you should enable compression to reduce bandwidth and storage costs. Add &lt;code&gt;compression: lz4&lt;/code&gt; to your tracer configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tracer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redpanda&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seed_brokers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;localhost:9092&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel-traces&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;schema-registry-json&lt;/span&gt;
    &lt;span class="na"&gt;schema_registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8081&lt;/span&gt;
    &lt;span class="na"&gt;compression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lz4&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;purchase-pipeline&lt;/span&gt;
    &lt;span class="na"&gt;sampling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;ratio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

&lt;span class="na"&gt;shutdown_delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;compression&lt;/code&gt; supports several codecs: &lt;code&gt;lz4&lt;/code&gt;, &lt;code&gt;snappy&lt;/code&gt;, &lt;code&gt;gzip&lt;/code&gt;, &lt;code&gt;zstd&lt;/code&gt;, and &lt;code&gt;none&lt;/code&gt;. The &lt;code&gt;lz4&lt;/code&gt; codec provides a good balance between compression ratio and CPU overhead.&lt;/p&gt;

&lt;p&gt;Keep in mind that &lt;code&gt;shutdown_delay&lt;/code&gt; is a root-level field in the pipeline configuration, not inside the &lt;code&gt;tracer&lt;/code&gt; block. It gives the tracer time to flush remaining spans before the process exits. Without it, you may lose traces that are still buffered when the pipeline shuts down. While the shutdown delay is in effect, the HTTP metrics endpoint continues to be available for scraping and any active tracers are free to flush remaining traces.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: The &lt;a href="https://docs.redpanda.com/redpanda-connect/configuration/about/" rel="noopener noreferrer"&gt;Redpanda Connect documentation&lt;/a&gt; recommends a &lt;code&gt;sampling.ratio&lt;/code&gt; between &lt;code&gt;0.01&lt;/code&gt; and &lt;code&gt;0.1&lt;/code&gt; for high-throughput production environments. I've settled on starting at &lt;code&gt;0.05&lt;/code&gt; (5%), as a reasonable default that balances observability with overhead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, I showed you how to configure tracer components in Redpanda Connect for distributed tracing. You set up a multi-step pipeline, configured the native Jaeger tracer for local development, and then switched to the OpenTelemetry Collector tracer for a backend-agnostic approach. You also explored sampling strategies to control trace volume in production, configured schema registry serialization for downstream consumers, and added compression and shutdown delay for production readiness.&lt;/p&gt;

&lt;p&gt;Because trace data lands in the &lt;code&gt;otel-traces&lt;/code&gt; topic just like any other event, you can consume it the same way, correlating traces across pipeline instances or routing it downstream for further processing. &lt;/p&gt;

&lt;p&gt;You can find the demo resources for this tutorial in &lt;a href="https://github.com/SystemCraftsman/redpanda-connect-configuring-tracer-components-demo" rel="noopener noreferrer"&gt;this GitHub repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>redpanda</category>
      <category>tracing</category>
    </item>
    <item>
      <title>Winning bids in real-time: Building high-throughput AdTech RTB pipelines with Redpanda, Iceberg, and AI</title>
      <dc:creator>Aykut Bulgu</dc:creator>
      <pubDate>Mon, 20 Jul 2026 21:19:41 +0000</pubDate>
      <link>https://dev.to/mabulgu/winning-bids-in-real-time-building-high-throughput-adtech-rtb-pipelines-with-redpanda-iceberg-3mck</link>
      <guid>https://dev.to/mabulgu/winning-bids-in-real-time-building-high-throughput-adtech-rtb-pipelines-with-redpanda-iceberg-3mck</guid>
      <description>&lt;p&gt;In programmatic advertising, every millisecond counts. Real-time bidding (RTB) systems have to compete in auctions and return a bid in sub-100ms windows to win impressions, while simultaneously capturing clickstream events like views and device/campaign metadata to power audience segmentation, personalization, and model retraining. I wanted to figure out how to do both at scale, without latency spikes.&lt;/p&gt;

&lt;p&gt;To do that, I built an RTB-style pipeline where clickstream events are ingested with &lt;a href="https://docs.redpanda.com/redpanda-connect/home/" rel="noopener noreferrer"&gt;Redpanda Connect&lt;/a&gt;, written to a data lake as Apache Iceberg tables using &lt;a href="https://docs.redpanda.com/current/manage/iceberg/about-iceberg-topics/" rel="noopener noreferrer"&gt;Redpanda Iceberg Topics&lt;/a&gt;, and then used to train and serve a small AI scoring model that feeds predictions back into a bidder loop.&lt;/p&gt;

&lt;p&gt;My goal wasn't to fully implement OpenRTB end-to-end, but to build a high-throughput, low-latency pipeline skeleton that could grow into a production RTB architecture. Here's how I put it together, so you can follow along and build your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  RTB bidding optimization
&lt;/h2&gt;

&lt;p&gt;Imagine an RTB pipeline with three recurring problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency spikes during traffic bursts (you miss auctions when you exceed the response window).&lt;/li&gt;
&lt;li&gt;High infrastructure cost from overprovisioning just to survive peak load.&lt;/li&gt;
&lt;li&gt;Slow model refresh because historical data lives in silos and is painful to query and retrain from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to improve the system so it can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingest clickstream events (views/clicks/device/campaign) in real time.&lt;/li&gt;
&lt;li&gt;Store the events cost-effectively as Iceberg tables for analytics and model training.&lt;/li&gt;
&lt;li&gt;Use AI to predict impression value (CTR proxy/value score).&lt;/li&gt;
&lt;li&gt;Stream predictions to the bidder so it can make faster and smarter decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the rough architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FcpFjdeS.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.imgur.com%2FcpFjdeS.png" alt="Architecture diagram" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To revamp the system, you'll implement the following components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redpanda Connect handles HTTP ingestion of bid requests and acts as the "glue", consuming requests and producing predictions.&lt;/li&gt;
&lt;li&gt;Redpanda Iceberg Topics streams data into Iceberg tables in object storage. Redpanda writes the data in Iceberg-compatible format, making it queryable with Iceberg clients.&lt;/li&gt;
&lt;li&gt;Spark/Jupyter (from the lab) queries Iceberg tables and produces a dataset for training.&lt;/li&gt;
&lt;li&gt;A tiny inference service scores bid requests and publishes &lt;code&gt;bid_predictions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A simple bidder loop consumes predictions and decides how aggressively to bid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;To keep this tutorial simple and repeatable, you'll use &lt;a href="https://github.com/redpanda-data/redpanda-labs.git" rel="noopener noreferrer"&gt;Redpanda’s official Iceberg Docker Compose lab&lt;/a&gt; as the base environment. It includes Redpanda, MinIO (S3-compatible storage), and Spark + Jupyter configured to query Iceberg tables.&lt;/p&gt;

&lt;p&gt;You'll also need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; and &lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.redpanda.com/current/get-started/rpk/" rel="noopener noreferrer"&gt;rpk CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;Python 3.11 or newer&lt;/a&gt; (for a tiny inference service and generators)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bringing up the Iceberg Playground (Redpanda, MinIO, Spark/Jupyter)
&lt;/h3&gt;

&lt;p&gt;To start, clone the labs repo and enter the project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/redpanda-data/redpanda-labs.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;redpanda-labs/docker-compose/iceberg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lab expects Redpanda &amp;gt;= 24.3.1. Define the required Redpanda versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REDPANDA_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v25.3.2
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REDPANDA_CONSOLE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v3.3.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the environment. This provides a complete pipeline from producing data to querying Iceberg tables in Spark/Jupyter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create an rpk profile for the lab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk profile create docker-compose-iceberg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin_api.addresses&lt;span class="o"&gt;=&lt;/span&gt;localhost:19644 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;brokers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost:19092 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;schema_registry.addresses&lt;span class="o"&gt;=&lt;/span&gt;localhost:18081
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating the RTB topics
&lt;/h3&gt;

&lt;p&gt;In this section, you'll create three topics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clickstream&lt;/code&gt;&lt;/strong&gt; (Iceberg-enabled)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bid_requests&lt;/code&gt;&lt;/strong&gt; (messages to score)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bid_predictions&lt;/code&gt;&lt;/strong&gt; (scored output) &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;code&gt;clickstream&lt;/code&gt;, start with &lt;code&gt;key_value&lt;/code&gt; mode because it’s the easiest path for HTTP ingestion (you can POST JSON and treat it as the "value"). &lt;/p&gt;

&lt;p&gt;Run the following set of commands to create the topics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk topic create clickstream &lt;span class="nt"&gt;--topic-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redpanda.iceberg.mode&lt;span class="o"&gt;=&lt;/span&gt;key_value &lt;span class="p"&gt;;&lt;/span&gt;
rpk topic create bid_requests &lt;span class="p"&gt;;&lt;/span&gt;
rpk topic create bid_predictions &lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Iceberg Topics are controlled via &lt;code&gt;redpanda.iceberg.mode&lt;/code&gt;. Once you produce to an Iceberg-enabled topic, the data becomes available in object storage for Iceberg clients to consume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running clickstream ingestion with Redpanda Connect
&lt;/h3&gt;

&lt;p&gt;In AdTech, clickstream often arrives via HTTP collectors. Redpanda Connect has an &lt;code&gt;http_server&lt;/code&gt; input for receiving POSTed events.&lt;/p&gt;

&lt;p&gt;Select a working directory on your local computer, create a YAML file called &lt;code&gt;connect-clickstream.yaml&lt;/code&gt;, then paste the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;http_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4196&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/events&lt;/span&gt;
    &lt;span class="na"&gt;allowed_verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;POST&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Keep it simple: accept JSON and pass through.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;root = this&lt;/span&gt;

&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka_franz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seed_brokers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redpanda:9092"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clickstream"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kafka_franz&lt;/code&gt; is a Kafka output in Redpanda Connect.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In Redpanda Cloud documentation, &lt;code&gt;kafka_franz&lt;/code&gt; is marked deprecated in favor of unified Redpanda components. If your target environment is Cloud, consider switching outputs accordingly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run Redpanda Connect as a container on the same Docker network as the lab (you may need to adjust the network name depending on your Docker Compose project name):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; redpanda-labs_default &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 4196:4196 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/connect-clickstream.yaml:/connect.yaml:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  docker.redpanda.com/redpandadata/connect:latest &lt;span class="se"&gt;\&lt;/span&gt;
  run /connect.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, send a couple of sample clickstream events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:4196/events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"user_id":101,"ad_id":55,"campaign_id":9,"event_type":"click","clicked":1,"ts":"2025-12-17T09:00:00Z"}'&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:4196/events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"user_id":102,"ad_id":55,"campaign_id":9,"event_type":"view","clicked":0,"ts":"2025-12-17T09:00:01Z"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify the generated clickstream data, consume the &lt;code&gt;clickstream&lt;/code&gt; data with the following command in a new terminal window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk topic consume clickstream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"topic"&lt;/span&gt;: &lt;span class="s2"&gt;"clickstream"&lt;/span&gt;,
  &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:101,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ad_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:55,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;campaign_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:9,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;event_type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;clicked&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ts&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;2025-12-17T09:00:00Z&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;,
  &lt;span class="s2"&gt;"timestamp"&lt;/span&gt;: ...,
  &lt;span class="s2"&gt;"partition"&lt;/span&gt;: 0,
  &lt;span class="s2"&gt;"offset"&lt;/span&gt;: 0
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"topic"&lt;/span&gt;: &lt;span class="s2"&gt;"clickstream"&lt;/span&gt;,
  &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:102,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ad_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:55,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;campaign_id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:9,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;event_type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;view&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;clicked&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:0,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;ts&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;2025-12-17T09:00:01Z&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;,
  &lt;span class="s2"&gt;"timestamp"&lt;/span&gt;: ...,
  &lt;span class="s2"&gt;"partition"&lt;/span&gt;: 0,
  &lt;span class="s2"&gt;"offset"&lt;/span&gt;: 0
&lt;span class="o"&gt;}&lt;/span&gt;
...output omitted...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verifying the Iceberg Writes
&lt;/h3&gt;

&lt;p&gt;On your browser, open &lt;code&gt;http://localhost:9001/browser&lt;/code&gt; and enter the credentials &lt;code&gt;minio / minio123&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4uj7ytggp79ktxvnogf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4uj7ytggp79ktxvnogf.png" alt="Minio login page" width="799" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open another tab and navigate to &lt;code&gt;http://localhost:8888&lt;/code&gt;, which is the page for Jupyter Labs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F911trju95ep8zff37w9z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F911trju95ep8zff37w9z.png" alt="Jupyter Labs" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The lab notebook should guide you through querying Iceberg tables created from Redpanda topics.&lt;/p&gt;

&lt;p&gt;You can also do a quick sanity check with Spark SQL. Run the following command to start &lt;code&gt;spark-iceberg&lt;/code&gt; and &lt;code&gt;spark-sql&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; spark-iceberg spark-sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the open interface, you can query the Iceberg table.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; the exact catalog/table naming is shown in the lab notebook; the lab demonstrates querying &lt;code&gt;lab.redpanda.&amp;lt;topic&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Building a "bid value" model from Iceberg data
&lt;/h3&gt;

&lt;p&gt;To create a small “bid value” model, you need to first prepare the data by parsing it from JSON and then save it as a &lt;code&gt;parquet&lt;/code&gt; file on the disk of the Jupyter Labs container. Then you can train the model using the saved training dataset.&lt;/p&gt;

&lt;h4&gt;
  
  
  Preparing the data
&lt;/h4&gt;

&lt;p&gt;At this point, you’ve validated the base path: HTTP -&amp;gt; Redpanda -&amp;gt; Iceberg. The next challenge is to train a model on fresh behavioral data and turn it into a low-latency scoring service.&lt;/p&gt;

&lt;p&gt;Because you created the Iceberg topic in &lt;code&gt;key_value&lt;/code&gt; mode, the Iceberg table stores your payload in a binary &lt;code&gt;value&lt;/code&gt; column (plus a &lt;code&gt;redpanda&lt;/code&gt; struct that holds record metadata).&lt;/p&gt;

&lt;p&gt;That means the training workflow has two phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extract:&lt;/strong&gt; Cast/decode value and parse your JSON into typed columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train:&lt;/strong&gt; Fit a lightweight model and save an artifact you can load in an API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go back to Jupyter in your browser and create a new Python notebook. In the first cell, load the Iceberg table. In this lab, tables are queryable as &lt;code&gt;lab.redpanda.&amp;lt;topic&amp;gt;&lt;/code&gt; (the lab explicitly shows querying &lt;code&gt;lab.redpanda.value_schema_id_prefix&lt;/code&gt;, so your &lt;code&gt;clickstream&lt;/code&gt; topic will be &lt;code&gt;lab.redpanda.clickstream&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Enter the following script in the first row and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;clickstream_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lab.redpanda.clickstream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;clickstream_raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;printSchema&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a schema that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;redpanda&lt;/code&gt; struct column (metadata)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;value&lt;/code&gt; column (binary payload)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To look at the raw payload, copy and paste the following script content in the second row of the notebook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clickstream_raw&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redpanda.timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rp_timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your events were JSON, you should see JSON strings in &lt;code&gt;json_value&lt;/code&gt;. If you see null or an error, it usually means one of two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You didn't actually publish JSON (unlikely in your curl case).&lt;/li&gt;
&lt;li&gt;The bytes aren't UTF-8, so the cast doesn't decode cleanly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you have to paste the JSON payload into typed columns. To do that, you need to define the schema you expect from your clickstream collector. In your Jupyter notebook, add the following and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;from_json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StructType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StructField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IntegerType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringType&lt;/span&gt;

&lt;span class="n"&gt;payload_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StructType&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;StructField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntegerType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;StructField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntegerType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;StructField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;campaign_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntegerType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;StructField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;StructField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntegerType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;StructField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To parse the value, add the following script and run it on your notebook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clickstream_raw&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;from_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;payload_schema&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;e&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;e.*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;campaign_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should be looking for clean columns like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;ad_id&lt;/code&gt;, &lt;code&gt;campaign_id&lt;/code&gt; as integers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event_type&lt;/code&gt; as a string&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clicked&lt;/code&gt; as 0/1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the &lt;code&gt;clicked&lt;/code&gt; event is sometimes missing, you can default it (for demo) to 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lit&lt;/span&gt;

&lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this tutorial, you won't train directly over millions of rows. You should take a sample so the notebook stays fast and repeatable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;training_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;training_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/jovyan/work/training_clickstream.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;training_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;training_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;training_path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you have a stable dataset on disk inside the Jupyter container.&lt;/p&gt;

&lt;h4&gt;
  
  
  Training the Model
&lt;/h4&gt;

&lt;p&gt;To train the model, you'll use a logistic regression pipeline with one-hot encoding for &lt;code&gt;event_type&lt;/code&gt;. This is intentionally small and "demo-grade," but it's enough to prove the pipeline. &lt;/p&gt;

&lt;p&gt;In the same Jupyter notebook, copy and paste the following content into the next available row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# If needed in the notebook environment:
# %pip install -q scikit-learn joblib pandas pyarrow
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;train_pd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;training_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_pd&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;campaign_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_pd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.compose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ColumnTransformer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.preprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OneHotEncoder&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.pipeline&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pipeline&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.linear_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LogisticRegression&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;roc_auc_score&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;joblib&lt;/span&gt;

&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;pre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ColumnTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;transformers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OneHotEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle_unknown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passthrough&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;campaign_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pre&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_iter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict_proba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;)[:,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUC:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;roc_auc_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then save the artifact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;artifact_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/jovyan/work/bid_value_model.joblib&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;joblib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifact_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;artifact_path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have a model artifact that's easy to ship into a tiny inference service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serving the model as an inference endpoint
&lt;/h3&gt;

&lt;p&gt;Now that you have a trained model artifact (&lt;code&gt;bid_value_model.joblib&lt;/code&gt;), you need to make it usable by your online RTB path. To do so, you'll copy the model artifact out of the Jupyter container, then run the pre-created inference service.&lt;/p&gt;

&lt;h4&gt;
  
  
  Copy the model artifact out of the Jupyter container
&lt;/h4&gt;

&lt;p&gt;You should have saved the artifact inside the Jupyter/Spark container at: &lt;code&gt;/home/jovyan/work/bid_value_model.joblib&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;On your host machine, open a terminal in the directory where you want to keep your inference service files and copy the model out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;cp &lt;/span&gt;spark-iceberg:/home/jovyan/work/bid_value_model.joblib ./bid_value_model.joblib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do a quick sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; ./bid_value_model.joblib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a non-zero file size (usually KBs–MBs depending on the pipeline).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; If the container name isn't &lt;code&gt;spark-iceberg&lt;/code&gt;, run &lt;code&gt;docker ps&lt;/code&gt; and replace it with whatever your lab uses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Run the inference service
&lt;/h4&gt;

&lt;p&gt;Run the following command on your terminal to clone the inference service demo, which uses FastAPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/SystemCraftsman/redpanda-rtb-iceberg-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate into the cloned repository directory and create a Python virtual environment and activate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, install the demo project requirements by executing the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since you have all the required dependencies installed now, you can run the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvicorn inference_service:app &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Verifying the inference service
&lt;/h4&gt;

&lt;p&gt;In a new terminal, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:8000/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the scoring with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/score &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"ad_id":55,"campaign_id":9,"event_type":"click"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your output should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"p_click"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.37&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get a &lt;code&gt;500&lt;/code&gt; error, it usually means one of these things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model file wasn’t found (&lt;code&gt;bid_value_model.joblib&lt;/code&gt; in the same directory, or wrong path)&lt;/li&gt;
&lt;li&gt;A dependency mismatch (e.g., scikit-learn version issues)&lt;/li&gt;
&lt;li&gt;Your request fields don’t match the feature names/types the pipeline expects&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scoring bid requests in-stream
&lt;/h3&gt;

&lt;p&gt;In a terminal on your host, produce a couple of requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"request_id":"req-1","ad_id":55,"campaign_id":9,"event_type":"view"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | rpk topic produce bid_requests &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'%v\n'&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"request_id":"req-2","ad_id":55,"campaign_id":9,"event_type":"click"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | rpk topic produce bid_requests &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'%v\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to consume these requests via a Redpanda Connect connector. Create a YAML file called &lt;code&gt;connect-score-bids.yaml&lt;/code&gt; and paste the following data to configure this connector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka_franz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seed_brokers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redpanda:9092"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;topics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bid_requests"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;consumer_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bid-scorer"&lt;/span&gt;

&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Store the original request so we can merge it back after the HTTP call.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;meta original_request = content()&lt;/span&gt;

    &lt;span class="c1"&gt;# Call the inference API.&lt;/span&gt;
    &lt;span class="c1"&gt;# Important: because this Connect pipeline runs in Docker, "localhost" would refer to the container.&lt;/span&gt;
    &lt;span class="c1"&gt;# On macOS/Windows, use host.docker.internal to reach your host machine.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://host.docker.internal:8000/score"&lt;/span&gt;
        &lt;span class="na"&gt;verb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POST&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

    &lt;span class="c1"&gt;# Merge original request + score response into one enriched event&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mapping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;let req = meta("original_request").parse_json()&lt;/span&gt;
        &lt;span class="s"&gt;let resp = this.parse_json()&lt;/span&gt;
        &lt;span class="s"&gt;root = req&lt;/span&gt;
        &lt;span class="s"&gt;root.p_click = resp.p_click&lt;/span&gt;

&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kafka_franz&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;seed_brokers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redpanda:9092"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bid_predictions"&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${! json("request_id") }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run Redpanda Connect on the same Docker network as the lab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt; redpanda-labs_default &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/connect-score-bids.yaml:/connect.yaml:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  docker.redpanda.com/redpandadata/connect:latest &lt;span class="se"&gt;\&lt;/span&gt;
  run /connect.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave this terminal open and the container running as it's your scoring pipeline.&lt;/p&gt;

&lt;p&gt;To verify the bid predictions are being produced, open a new terminal and consume &lt;code&gt;bid_predictions&lt;/code&gt; topic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpk topic consume bid_predictions &lt;span class="nt"&gt;-n&lt;/span&gt; 2 &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'%k %v\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see keys and enriched values similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;req&lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"req-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ad_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"campaign_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"view"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"p_click"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;req&lt;/span&gt;&lt;span class="mi"&gt;-2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"req-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ad_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"campaign_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"event_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"click"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"p_click"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.37&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can use a bidder script written in Python to consume the data. Here's an example &lt;code&gt;bidder.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KafkaConsumer&lt;/span&gt;

&lt;span class="n"&gt;BROKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:19092&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TOPIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bid_predictions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KafkaConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TOPIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bootstrap_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BROKER&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;auto_offset_reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;earliest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;enable_auto_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value_deserializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bidder is listening for predictions...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="n"&gt;req_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p_click&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BID_AGGRESSIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;bid_multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BID_CONSERVATIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;bid_multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] p_click=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (multiplier=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bid_multiplier&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After you run &lt;code&gt;bidder.py&lt;/code&gt;, you have a tiny “decision loop” sitting on top of your streaming pipeline. The script continuously consumes messages from the &lt;code&gt;bid_predictions&lt;/code&gt; topic, extracts the model’s &lt;code&gt;p_click&lt;/code&gt; score, and applies a simple threshold rule to choose a bid strategy. If &lt;code&gt;p_click&lt;/code&gt; is above 0.30, it prints an aggressive decision as higher bid multiplier; otherwise it prints a conservative decision as lower multiplier. In a real RTB bidder, you’d plug in your actual pricing logic (budget constraints, floor prices, pacing, frequency caps), but the core idea is the same: stream in predictions, make a decision immediately, and respond fast enough to win auctions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, I showed you how to build an RTB-style pipeline that separates high-throughput learning and low-latency decisioning. You've created a streaming backbone with Redpanda that can absorb traffic spikes, a lakehouse layer that keeps storage and historical analysis cost-effective, and an online scoring loop that can evolve independently as your features and models improve. It's not a production RTB architecture, but it's a skeleton you could grow into one.&lt;/p&gt;

&lt;p&gt;You can find all the relevant code for the demo on &lt;a href="https://github.com/SystemCraftsman/redpanda-rtb-iceberg-demo" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>redpanda</category>
      <category>iceberg</category>
      <category>ai</category>
      <category>adtech</category>
    </item>
  </channel>
</rss>
