""" IoT Consumer — Flask app that reads IoT events from Kafka (TLS + SASL) and inserts them into ClickHouse. Env vars (set by Terraform): KAFKA_BROKERS — bootstrap server (host:port) KAFKA_TOPIC — topic name KAFKA_USERNAME — SASL username KAFKA_PASSWORD — SASL password KAFKA_CA_CRT — CA certificate (PEM) KAFKA_USER_CRT — user certificate (PEM) KAFKA_USER_KEY — user private key (PEM) KAFKA_GROUP_ID — consumer group id CH_HOST — ClickHouse host CH_PORT — ClickHouse port (default 8123) CH_USER — ClickHouse user CH_PASSWORD — ClickHouse password CH_DATABASE — ClickHouse database name """ import json import os import tempfile import threading from flask import Flask, jsonify from kafka import KafkaConsumer import clickhouse_connect app = Flask(__name__) # --- Config --- KAFKA_BROKERS = os.environ["KAFKA_BROKERS"] KAFKA_TOPIC = os.environ["KAFKA_TOPIC"] KAFKA_USERNAME = os.environ["KAFKA_USERNAME"] KAFKA_PASSWORD = os.environ["KAFKA_PASSWORD"] KAFKA_CA_CRT = os.environ.get("KAFKA_CA_CRT", "") KAFKA_USER_CRT = os.environ.get("KAFKA_USER_CRT", "") KAFKA_USER_KEY = os.environ.get("KAFKA_USER_KEY", "") KAFKA_GROUP_ID = os.environ.get("KAFKA_GROUP_ID", "iot-consumer-group") CH_HOST = os.environ["CH_HOST"] CH_PORT = int(os.environ.get("CH_PORT", 8123)) CH_USER = os.environ["CH_USER"] CH_PASSWORD = os.environ["CH_PASSWORD"] CH_DATABASE = os.environ["CH_DATABASE"] # --- TLS certs → temp files --- _cert_dir = tempfile.mkdtemp(prefix="kafka-certs-") def _write_cert(name, content): if not content: return None path = os.path.join(_cert_dir, name) with open(path, "w") as f: f.write(content) return path _ca_file = _write_cert("ca.crt", KAFKA_CA_CRT) _cert_file = _write_cert("user.crt", KAFKA_USER_CRT) _key_file = _write_cert("user.key", KAFKA_USER_KEY) events_consumed = 0 errors_count = 0 ch_client = None def get_ch_client(): global ch_client if ch_client is None: ch_client = clickhouse_connect.get_client( host=CH_HOST, port=CH_PORT, username=CH_USER, password=CH_PASSWORD, database=CH_DATABASE, ) ch_client.command(""" CREATE TABLE IF NOT EXISTS iot_events ( event_id String, sensor_id String, device_type String, location String, value Float64, unit String, timestamp DateTime64(3) ) ENGINE = MergeTree() ORDER BY (timestamp, sensor_id) """) print("[CONSUMER] ClickHouse table ready") return ch_client def insert_batch(events): if not events: return rows = [[ e["event_id"], e["sensor_id"], e["device_type"], e["location"], e["value"], e["unit"], e["timestamp"], ] for e in events] try: client = get_ch_client() client.insert("iot_events", rows, column_names=["event_id", "sensor_id", "device_type", "location", "value", "unit", "timestamp"]) except Exception as e: print(f"[CONSUMER] Insert error: {e}") raise def consume_loop(): global events_consumed, errors_count kwargs = { "bootstrap_servers": KAFKA_BROKERS, "security_protocol": "SASL_SSL", "sasl_mechanism": "PLAIN", "sasl_plain_username": KAFKA_USERNAME, "sasl_plain_password": KAFKA_PASSWORD, "group_id": KAFKA_GROUP_ID, "auto_offset_reset": "earliest", "value_deserializer": lambda m: json.loads(m.decode("utf-8")), "max_poll_records": 20, } if _ca_file: kwargs["ssl_cafile"] = _ca_file if _cert_file: kwargs["ssl_certfile"] = _cert_file if _key_file: kwargs["ssl_keyfile"] = _key_file kwargs["ssl_check_hostname"] = False consumer = KafkaConsumer(KAFKA_TOPIC, **kwargs) print(f"[CONSUMER] Listening on topic '{KAFKA_TOPIC}'...") batch = [] for message in consumer: try: event = message.value batch.append(event) events_consumed += 1 if len(batch) >= 10: insert_batch(batch) print(f"[CONSUMER] Inserted {len(batch)} events into ClickHouse") batch = [] except Exception as e: errors_count += 1 print(f"[CONSUMER] Error: {e}") @app.route("/") def index(): query = "SELECT count() FROM iot_events" try: client = get_ch_client() total = client.query(query).result_rows[0][0] except Exception: total = 0 return jsonify({ "service": "iot-consumer", "status": "running", "kafka_topic": KAFKA_TOPIC, "events_consumed": events_consumed, "events_in_db": total, "errors": errors_count, }) @app.route("/health") def health(): return jsonify({"status": "ok", "events_consumed": events_consumed}) if __name__ == "__main__": threading.Thread(target=consume_loop, daemon=True).start() port = int(os.environ.get("PORT", 5000)) app.run(host="0.0.0.0", port=port)