Skip to content
Fondazione ITS INCOM

Examples · Internet of Things

A gateway log, explained

Sixty-five lines from the log of a classroom IoT gateway go in, and incidents come out as JSON, each with the lines it is built on. Your code checks that every line exists and every quote is on it, lists the warnings left out, and only then asks for a short note for the technician.

Intermediate · 35 min

The idea

A gateway collects the readings of the sensors in a room and sends them to the broker. It also writes a log: hundreds of lines a day, most of them “everything is fine”. When something goes wrong, the useful lines are scattered among the others, and one problem often shows up as ten lines in three places.

A model reads the whole log and groups the lines into incidents. That is the useful part, and also the risky one: it can point at the wrong line, quote something that is not there, or leave a warning out. So:

  1. the model groups the events into incidents, as JSON, with the line numbers and a quote from each line;
  2. your code checks every line number and every quote, and lists any warning or error that is in no incident;
  3. only the lines that passed go into a second request: a short note for the technician.

What you need

A key and the openai library, as in Your first API call in Python.

The log

Two hours of gw-lab3, the gateway of Lab 3 in Legnano, on a Wednesday morning. Save it as gateway.log. Read it before you run anything: how many problems do you find?

2026-10-07 07:58:01 INFO  [system] gw-lab3 running, firmware 2.3.0, uptime 6d 21h
2026-10-07 07:58:01 INFO  [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 07:59:12 INFO  [zigbee] air-12 report: 21.4 C, 47 %, battery 2.41 V
2026-10-07 07:59:12 WARN  [zigbee] air-12 battery low (2.41 V < 2.50 V)
2026-10-07 08:00:00 INFO  [bridge] forwarded 24 readings in the last 5 min
2026-10-07 08:00:03 INFO  [zigbee] door-3 report: open
2026-10-07 08:02:44 DEBUG [wifi] signal -67 dBm
2026-10-07 08:05:00 INFO  [bridge] forwarded 23 readings in the last 5 min
2026-10-07 08:06:30 INFO  [dhcp] lease renewed: 10.20.3.41 for 12 h
2026-10-07 08:07:15 WARN  [tls] broker certificate expires in 6 days (2026-10-13)
2026-10-07 08:08:51 INFO  [zigbee] door-3 report: closed
2026-10-07 08:10:00 INFO  [bridge] forwarded 24 readings in the last 5 min
2026-10-07 08:11:52 DEBUG [wifi] signal -79 dBm
2026-10-07 08:12:07 WARN  [wifi] wlan0 disconnected (reason 4: inactivity)
2026-10-07 08:12:07 ERROR [mqtt] connection lost: network unreachable
2026-10-07 08:12:08 INFO  [bridge] broker offline, queueing readings
2026-10-07 08:12:12 INFO  [mqtt] reconnect attempt 1 in 5 s
2026-10-07 08:12:22 INFO  [mqtt] reconnect attempt 2 in 10 s
2026-10-07 08:12:42 INFO  [mqtt] reconnect attempt 3 in 20 s
2026-10-07 08:13:02 INFO  [mqtt] reconnect attempt 4 in 40 s
2026-10-07 08:13:38 INFO  [wifi] wlan0 connected to ITS-LAB, signal -70 dBm
2026-10-07 08:13:43 INFO  [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 08:13:43 INFO  [bridge] sent 18 queued readings
2026-10-07 08:15:00 INFO  [bridge] forwarded 19 readings in the last 5 min
2026-10-07 08:15:31 DEBUG [ble] ignored advertisement from 4C:21:9E:0A:77:12 (unknown device)
2026-10-07 08:20:00 INFO  [bridge] forwarded 24 readings in the last 5 min
2026-10-07 08:20:04 WARN  [zigbee] air-12 no answer to poll (timeout 3 s), retry 1/5
2026-10-07 08:22:10 DEBUG [ble] ignored advertisement from 4C:21:9E:0A:77:12 (unknown device)
2026-10-07 08:25:00 INFO  [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:25:04 WARN  [zigbee] air-12 no answer to poll (timeout 3 s), retry 2/5
2026-10-07 08:30:00 INFO  [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:30:04 WARN  [zigbee] air-12 no answer to poll (timeout 3 s), retry 3/5
2026-10-07 08:31:47 INFO  [zigbee] door-3 report: open
2026-10-07 08:35:00 INFO  [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:35:04 WARN  [zigbee] air-12 no answer to poll (timeout 3 s), retry 4/5
2026-10-07 08:40:00 INFO  [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:40:04 WARN  [zigbee] air-12 no answer to poll (timeout 3 s), retry 5/5
2026-10-07 08:40:04 ERROR [zigbee] air-12 marked offline after 5 failed polls
2026-10-07 08:45:00 INFO  [bridge] forwarded 20 readings in the last 5 min
2026-10-07 08:50:00 INFO  [bridge] forwarded 20 readings in the last 5 min
2026-10-07 08:55:00 INFO  [bridge] forwarded 20 readings in the last 5 min
2026-10-07 09:00:00 INFO  [update] firmware 2.3.1 available
2026-10-07 09:00:02 INFO  [update] downloading gw-fw-2.3.1.bin (14.2 MB)
2026-10-07 09:01:15 INFO  [update] download complete, sha256 OK
2026-10-07 09:01:16 INFO  [update] installing, the gateway will restart
2026-10-07 09:01:40 INFO  [system] rebooting
1970-01-01 00:00:09 INFO  [system] gw-lab3 running, firmware 2.3.1, uptime 0d 0h
1970-01-01 00:00:11 WARN  [config] unknown key mqtt.keep_alive, using the default (60 s)
1970-01-01 00:00:12 INFO  [wifi] wlan0 connected to ITS-LAB, signal -69 dBm
1970-01-01 00:00:14 ERROR [mqtt] TLS handshake failed: certificate is not yet valid
1970-01-01 00:00:15 INFO  [zigbee] network up, 6 of 7 devices answered (missing: air-12)
1970-01-01 00:00:19 INFO  [mqtt] reconnect attempt 1 in 5 s
2026-10-07 09:02:31 INFO  [ntp] clock set from pool.ntp.org (it was 1970-01-01 00:00:21)
2026-10-07 09:02:33 INFO  [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 09:05:00 INFO  [bridge] forwarded 17 readings in the last 5 min
2026-10-07 09:10:00 INFO  [bridge] forwarded 20 readings in the last 5 min
2026-10-07 09:12:28 DEBUG [ble] ignored advertisement from 4C:21:9E:0A:77:12 (unknown device)
2026-10-07 09:15:00 INFO  [bridge] forwarded 20 readings in the last 5 min
2026-10-07 09:40:58 DEBUG [wifi] signal -78 dBm
2026-10-07 09:41:10 WARN  [wifi] wlan0 disconnected (reason 4: inactivity)
2026-10-07 09:41:10 ERROR [mqtt] connection lost: network unreachable
2026-10-07 09:41:15 INFO  [mqtt] reconnect attempt 1 in 5 s
2026-10-07 09:41:29 INFO  [wifi] wlan0 connected to ITS-LAB, signal -78 dBm
2026-10-07 09:41:30 INFO  [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 09:41:30 INFO  [bridge] sent 3 queued readings

The program

Save it as explain_log.py, next to the log:

import json
import os
import re
import sys

from openai import OpenAI

client = OpenAI(
    base_url="https://api.ai.itsincom.org/v1",
    api_key=os.environ["API_KEY"],
)

model = sys.argv[1] if len(sys.argv) > 1 else "qwen3.8-27b"
lines = open("gateway.log").read().splitlines()
numbered = "\n".join(f"{n:3} | {line}" for n, line in enumerate(lines, 1))

schema = {
    "type": "object",
    "properties": {
        "incidents": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "title": {"type": "string"},
                    "severity": {"type": "string", "enum": ["high", "medium", "low"]},
                    "what_happened": {"type": "string"},
                    "evidence": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "line": {"type": "integer"},
                                "quote": {"type": "string"},  # copied from that line, word for word
                            },
                            "required": ["line", "quote"],
                        },
                    },
                },
                "required": ["title", "severity", "what_happened", "evidence"],
            },
        },
    },
    "required": ["incidents"],
}

answer = client.chat.completions.create(
    model=model,
    temperature=0,
    messages=[
        {"role": "system", "content": "You read the log of an IoT gateway for the technician. Group the events "
                                      "into incidents: one incident is one problem, with all the lines that "
                                      "belong to it, from the first sign to the end. Leave out what is normal. "
                                      "For every line you use, copy a short quote from it, word for word."},
        {"role": "user", "content": numbered},
    ],
    response_format={"type": "json_schema", "json_schema": {"name": "incidents", "schema": schema}},
)
if answer.choices[0].finish_reason != "stop":
    sys.exit("The answer was cut off: run it again.")
incidents = json.loads(answer.choices[0].message.content)["incidents"]

# Check 1: every line exists, and the quote is really on it.
used = set()
for incident in incidents:
    checked = []
    for e in incident["evidence"]:
        n, quote = e["line"], e["quote"].strip()
        if not 1 <= n <= len(lines):
            print(f"  line {n} does not exist")
        elif not quote or quote not in lines[n - 1]:
            print(f"  line {n}: «{quote}» is not on that line")
        else:
            checked.append(n)
    incident["lines"] = sorted(set(checked))
    used.update(checked)
    print(f"[{incident['severity']}] {incident['title']}  (lines {', '.join(map(str, incident['lines']))})")
    print(f"    {incident['what_happened']}")

# Check 2: every warning and every error belongs to an incident.
for n, line in enumerate(lines, 1):
    if (" WARN " in line or " ERROR " in line) and n not in used:
        print(f"NOT IN ANY INCIDENT: {n:3} | {line}")

# The note for the technician, written only from the checked incidents and their real lines.
facts = [{"title": i["title"], "severity": i["severity"], "lines": [lines[n - 1] for n in i["lines"]]}
         for i in incidents if i["lines"]]
answer = client.chat.completions.create(
    model=model,
    temperature=0,
    messages=[
        {"role": "system", "content": "Write a note for the technician who goes to the lab tomorrow morning: "
                                      "at most 5 short lines, plain text, the most urgent first, each with "
                                      "what to do. Use only these incidents."},
        {"role": "user", "content": json.dumps(facts, indent=1)},
    ],
)
note = answer.choices[0].message.content.strip()
print("\nNote for the technician:\n" + note)

# Check 3: at most 5 lines, and every time in the note is a time in the log.
if len(note.splitlines()) > 5:
    print(f"TOO LONG: {len(note.splitlines())} lines")
for time in re.findall(r"\b\d{2}:\d{2}\b", note):
    if not any(f" {time}:" in line for line in lines):
        print(f"NOT IN THE LOG: {time}")

Run it with python3 explain_log.py. With qwen3.8-27b it takes about a minute: the model reasons before each of the two answers.

What you should see

One of our runs:

[medium] air-12 battery low, then device goes offline  (lines 4, 27, 30, 32, 35, 37, 38, 51)
    The air-12 sensor's battery dropped below the 2.50 V threshold, and roughly 20 minutes later the device stopped answering polls. After five consecutive failed polls it was marked offline. It was still missing when the gateway came back up after the firmware update.
[medium] WiFi drop → MQTT connection lost → recovery (first occurrence)  (lines 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
    The WiFi signal degraded from -67 dBm to -79 dBm, then the interface dropped due to inactivity. The MQTT connection was lost, readings were queued, and four reconnect attempts were made over about 90 seconds. WiFi and MQTT both recovered, and the 18 queued readings were flushed.
[low] Broker TLS certificate expiring in 6 days  (lines 10)
    A warning that the broker's TLS certificate will expire on 2026-10-13, six days after this log entry. No action was taken in the log.
[medium] Post-restart: clock reset → TLS handshake failure → recovery  (lines 48, 50, 52, 53, 54)
    After the firmware update and reboot, the system clock was stuck at 1970-01-01. This caused the TLS handshake with the broker to fail (certificate 'not yet valid' because the clock was in the past). One reconnect attempt was made. The NTP service then corrected the clock, and the MQTT connection was established. A minor config warning about an unknown key also appeared at startup.
[medium] WiFi drop → MQTT connection lost → recovery (second occurrence)  (lines 59, 60, 61, 62, 63, 64, 65)
    The WiFi signal degraded to -78 dBm and the interface dropped again due to inactivity. MQTT was lost, one reconnect attempt was made, and both WiFi and MQTT recovered within about 20 seconds. Three queued readings were flushed.

Note for the technician:
Replace battery on air-12 (2.41 V, device offline after 5 failed polls).
Fix recurring WiFi inactivity drops (wlan0 disconnected twice, reason 4) – check AP or raise keep-alive.
Add NTP sync to boot sequence (clock reset to 1970 caused TLS handshake failure until 09:02).
Renew broker TLS certificate (expires 2026-10-13, 6 days out).

Good work, and every line passed the checks. Look at the battery: the warning on line 4 and the timeouts from line 27 are about 20 minutes apart, and the model put them together. Line 51, after the reboot, says that air-12 is still missing: it found that too.

The clock is the most interesting one. After the reboot the gateway thinks it is 1 January 1970, so the broker's certificate is “not yet valid”, and the connection fails until NTP sets the clock. The model explained it correctly in all six of our runs, with both models.

What the checks caught

A warning left out. We ran the program three times with gemma-4-26b. Every time, it left line 48 out of all incidents:

NOT IN ANY INCIDENT:  48 | 1970-01-01 00:00:11 WARN  [config] unknown key mqtt.keep_alive, using the default (60 s)

After the update, the gateway ignores a setting that someone chose on purpose. Maybe it does not matter; the technician should decide, not the model. Qwen3.8 put the line in an incident in all three runs; in one of them it called it “minor”.

Line numbers. In our six runs, every line number existed and every quote was on its line. That is because the program sends the lines with their numbers. We tried once without them, with each model:

  line 20261007 does not exist

Gemma used the date as a line number, for every line. Qwen3.8 counted the lines itself and lost count:

  line 26: «air-12 no answer to poll (timeout 3 s), retry 1/5» is not on that line

That quote is on line 27. Without the check, the technician would open the log at the wrong line. Without the numbers, the check rejected most of the lines: the numbers are what make the answer usable.

What nobody caught

Gemma or Qwen

gemma-4-26b qwen3.8-27b
Time for the whole program about 12 s 68 to 81 s
Warnings left out of every incident line 48, in 3 runs out of 3 none
Battery and timeouts in one incident 0 runs out of 3 2 runs out of 3
The note 3 or 4 short lines 4 or 5 lines, with values such as 2.41 V

Both models passed check 1 in every run. Gemma is about six times faster; Qwen3.8 connects more events into one incident. For a log you read every morning, run Gemma and let check 2 point at what it left out.

Good to know

Exercise

All examples