← Back to list

From Kafka Events to Real-Time Alerts: A Hands-On Guide with ksqlDB

Learn how to combine Kafka topics, AVRO schemas, and ksqlDB queries to detect suspicious patterns in real time for event monitoring.

Zeinab Dashti in CloudCraft · 2025-11-23 09:34 · 13 claps · 3.3 min read
#apache-kafka #data-streaming #cybersecurity #ksqldb #real-time-analytics
Open on Medium ↗
Wiki topics: GRW · Growth & Analytics 🔒 · Cybersecurity 🎮 · Gaming 🎬 · Film & Television

From Kafka Events to Real-Time Alerts: A Hands-On Guide with ksqlDB

Real-time data is everywhere: logins, payments, sensors, clicks. The value of this data often fades within minutes — or even seconds — if you don’t act on it.

You could write custom Kafka consumers and build an event-processing microservice, but that involves coding, deployment, and testing before you even get to the business logic.

This is where streaming SQL shines. With just SQL queries, you can continuously process events as they arrive, spot suspicious patterns, and trigger actions in real time.

Among the available tools, including Apache Flink SQL, Spark Structured Streaming, RisingWave, and ksqlDB, each has its strengths. Flink is now considered the most advanced option, and Confluent has shifted much of its focus there. But ksqlDB remains one of the easiest ways to get started, especially if you already use Kafka. It’s lightweight, simple to spin up, and ideal for proof-of-concepts or smaller real-time use cases that don’t require writing Java or Python.

In this article, I’ll show you how to use ksqlDB to detect brute-force login attempts in real time. We’ll build the environment with Docker, stream login events into Kafka, define a detection rule in SQL, and generate alerts that downstream systems can consume.

Detect Brute-force Login Attempts in Real Time

Step 1: Environment Setup

We’ll use Docker Compose to run ZooKeeper, Kafka broker, Schema Registry, ksqlDB server, and client.

services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    ports:
      - "2181:2181"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181

  kafka:
    image: confluentinc/cp-kafka:7.4.0
    ports:
      - "9092:9092"     # internal (Docker network)
      - "29092:29092"   # external (host -> container)
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181

      # Two listeners: one for containers, one for your laptop
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,PLAINTEXT_HOST://0.0.0.0:29092
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT

      # Single-broker friendly settings
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
    depends_on:
      - zookeeper

  schema-registry:
    image: confluentinc/cp-schema-registry:7.4.0
    ports:
      - "8081:8081"
    environment:
      SCHEMA_REGISTRY_HOST_NAME: schema-registry
      SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081
      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka:9092
    depends_on:
      - kafka

  ksqldb-server:
    image: confluentinc/cp-ksqldb-server:7.4.0
    ports:
      - "8088:8088"
    environment:
      KSQL_LISTENERS: http://0.0.0.0:8088
      KSQL_BOOTSTRAP_SERVERS: kafka:9092
      KSQL_KSQL_SCHEMA_REGISTRY_URL: http://schema-registry:8081
      KSQL_KSQL_SERVICE_ID: "ksql_local_"
      KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true"
      KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true"
    depends_on:
      - kafka
      - schema-registry

  ksqldb-cli:
    image: confluentinc/cp-ksqldb-cli:7.4.0
    container_name: ksqldb-cli
    depends_on:
      - ksqldb-server
    entrypoint: ["/bin/sh","-c","sleep infinity"]  # keep the container running
    tty: true
    volumes:
      - ./ksql:/scripts   # optional: put .sql files here

Step 2: Create Kafka Topic

Once the stack is started and services are all up and running, we create a Kafka topic for the login attempts

kafka-topics --bootstrap-server localhost:29092 --create --topic login_attempts --partitions 1 --replication-factor 1Step 3: Example Event Schema

The events will have the following schema:

{
  "user_id": "u123",
  "ip": "203.0.113.42",
  "success": false,
  "event_time": "2025-08-20T10:05:30.000Z",
  "ua": "Mozilla/5.0"
}

In this schema, event_time is the source-of-truth timestamp. And the data will be partitioned by user_id, ensuring accurate per-user aggregations.

Step 3: Define the ksqlDB Streams

Open ksqlDB CLI:

docker exec -it ksqldb-cli ksql http://ksqldb-server:8088

Create the source stream over your existing topic using AVRO:

SET 'auto.offset.reset'='earliest';

CREATE STREAM login_attempts_raw (
  user_id STRING KEY,
  ip STRING,
  success BOOLEAN,
  event_time TIMESTAMP,
  ua STRING
) WITH (
  KAFKA_TOPIC='login_attempts',
  VALUE_FORMAT='AVRO',
  PARTITIONS=1,
  TIMESTAMP='event_time'
);

By setting TIMESTAMP=event_time, KsqlDB uses that field as the Kafka record timestamp for windowing; otherwise, it uses processing time.

Step 4: Detection Rule (Windowed Aggregation)

We flag a user if they attempt three failed logins within five minutes. To do that, we need to check “any” 5-minute span (more sensitive than fixed 5-minute blocks) and emit “alerts” only when the count crosses 5.

CREATE TABLE failed_login_alerts
  WITH (KAFKA_TOPIC='failed_login_alerts', VALUE_FORMAT='AVRO') AS
SELECT
  user_id,
  COUNT(*)      AS fail_count,
  WINDOWSTART   AS window_start,
  WINDOWEND     AS window_end
FROM login_attempts_raw
WINDOW HOPPING (SIZE 5 MINUTES, ADVANCE BY 1 MINUTE, GRACE PERIOD 1 MINUTE)
WHERE success = false
GROUP BY user_id
HAVING COUNT(*) = 3
EMIT CHANGES;

This query creates a table indexed by user_id and window that updates as events arrive.

The ADVANCED BY 1 MINUTE creates a new hopping window that starts every minute, and GRACE PERIOD lets slightly late events still count.

Step 5: Watch Alerts Live

Run the following command and produce a burst of failures for a user.

SELECT * FROM failed_login_alerts EMIT CHANGES;

As soon as the entry records for a user match the query condition, you will see a record of the user.

Once you have the stream, consumers can read the failed_login_alerts table as “alert events” and respond to them.

Final Thoughts

Streaming SQL isn’t here to replace full-blown stream processing frameworks, but it fills an important gap: making real-time event processing accessible without heavy coding.

KsqlDB may not be the most advanced streaming SQL engine, but it’s still one of the simplest ways to turn Kafka events into real-time insights.

The brute-force detection example here is basic, yet it demonstrates how quickly you can transform raw events into actionable alerts with just SQL. For anyone starting with real-time processing, ksqlDB remains a practical entry point before moving on to heavier frameworks.


메타데이터
post_id
b25ee372a6c9
slug
from-kafka-events-to-real-time-alerts-a-hands-on-guide-with-ksqldb-b25ee372a6c9
url
https://medium.com/cloudnativepub/from-kafka-events-to-real-time-alerts-a-hands-on-guide-with-ksqldb-b25ee372a6c9
canonical_url
https://medium.com/cloudnativepub/from-kafka-events-to-real-time-alerts-a-hands-on-guide-with-ksqldb-b25ee372a6c9
author_url
https://medium.com/@zdb.dashti
status
ok
fetched_at
2026-07-15 01:43:35