🚧 WIP
This commit is contained in:
@@ -27,7 +27,7 @@ opcda-diagnozer/
|
||||
├── src/opcda_diagnozer/
|
||||
│ ├── __init__.py
|
||||
│ ├── __main__.py # python -m opcda_diagnozer
|
||||
│ ├── main.py # CLI: run / install / uninstall / check-config / probe / list-devices / list-tags
|
||||
│ ├── main.py # CLI: run / install / uninstall / check-config / probe / read / list-items
|
||||
│ ├── config.py # pydantic-free typed config (dataclasses) + YAML load + validation
|
||||
│ ├── events.py # DiagEvent, Severity, EventKind
|
||||
│ ├── quality.py # OPC DA quality decode tables
|
||||
@@ -44,6 +44,7 @@ opcda-diagnozer/
|
||||
│ ├── report/
|
||||
│ │ ├── base.py # Reporter protocol
|
||||
│ │ ├── jsonl.py # rotating JSONL file
|
||||
│ │ ├── textlog.py # plain-text anomaly log (human-readable, §9.1)
|
||||
│ │ ├── eventlog.py # Windows Event Log (WARN+)
|
||||
│ │ └── http.py # optional webhook
|
||||
│ └── service.py # pywin32 ServiceFramework wrapper
|
||||
@@ -82,7 +83,13 @@ Tasks:
|
||||
|
||||
1. `config.py` — YAML config (see `config.example.yaml` sketch in README), dataclass
|
||||
model, strict validation, `check-config` messages that explain exactly what is wrong.
|
||||
2. `quality.py` — decode tables (spec §7), `decode_quality(raw: int) -> QualityInfo`.
|
||||
Includes the **device-link indicators** (spec §6.1 / D16): `devices:` — each entry a
|
||||
human-readable device name plus its connection-indicator tag ItemID. Indicators are
|
||||
added to the polled tag set automatically; `check-config` warns when an indicator tag
|
||||
is also listed in `tags:` (it should not be) and fails on an empty device name.
|
||||
2. `quality.py` — decode tables (spec §7), `decode_quality(raw: int) -> QualityInfo`,
|
||||
plus the **clear-text status helper**: `status_text(raw) -> "NAME (0xNN)"` used by
|
||||
every reporter (spec §7 closing rule — e.g. `BAD / communication failure (0x18)`).
|
||||
3. `opc/base.py` — client abstraction:
|
||||
```python
|
||||
class OpcClient(Protocol):
|
||||
@@ -99,6 +106,14 @@ Tasks:
|
||||
6. `monitor/detectors.py` — D1–D15 as small pure functions/classes over
|
||||
`PollOutcome` (list[ReadResult] + rtt_ms + error), using an injected clock for
|
||||
testability. Rolling window (deque, size `window_polls`) for p95/streak logic.
|
||||
**D16 device-link detector** (spec §6.1): per configured indicator — quality
|
||||
not GOOD, a read error, or a GOOD read whose value is **not in the
|
||||
configurable `ok_values` list** (default `[true]`; string-equality compare
|
||||
of the rendered value, so inverted fault flags `[false, 0]` and string states
|
||||
`["RUN"]` both work) ⇒ `DEVICE_LINK_DOWN` (CRITICAL) carrying the device name,
|
||||
indicator tag, clear-text status and value; recovery (`DEVICE_LINK_UP`) when
|
||||
it reads GOOD with an ok value again. Edge-level per device, so flapping
|
||||
links don't spam (one event per transition).
|
||||
7. `monitor/poller.py` — the single COM thread; watchdog logic for hang detection
|
||||
(spec §10.4): a second thread timestamps poll starts/ends; on `hard_hang_s` the COM
|
||||
thread is abandoned and rebuilt.
|
||||
@@ -107,17 +122,26 @@ Tasks:
|
||||
hangs, quality flips) → powers the entire unit-test suite without COM/hardware.
|
||||
|
||||
**Exit:** `opcda-diagnozer run` (foreground console) produces correct events against the
|
||||
fake client; unit tests for all detectors.
|
||||
fake client; unit tests for all detectors — including D16 edge semantics (false /
|
||||
non-GOOD / read-error / misconfigured value / recovery) against scripted indicator reads.
|
||||
|
||||
## 5. Phase 2 — Reporters (≈1 day)
|
||||
|
||||
1. `report/jsonl.py` — rotation (size + backups), atomic-ish append, header line with
|
||||
config digest for provenance.
|
||||
2. `report/eventlog.py` — pywin32 `win32evtlog` usage; map severities; source
|
||||
2. `report/textlog.py` — **plain-text anomaly log** (spec §9.1): `anomalies-YYYYMMDD.log`
|
||||
in the same directory as the JSONL files, one human-readable line per detected
|
||||
anomaly (`timestamp SEVERITY KIND message`) with clear-text status `NAME (0xNN)`.
|
||||
Anomalies only — heartbeats/CONNECTED are filtered out (a small `is_anomaly(event)`
|
||||
predicate on `EventKind`); recoveries (D2, `DEVICE_LINK_UP`, quality back to GOOD)
|
||||
**are** logged, so the file reads as an incident narrative. Daily + size rotation
|
||||
with `backups` kept, same scheme as the JSONL reporter. Every line corresponds to
|
||||
a JSONL record (text = convenience view, JSONL = source of truth).
|
||||
3. `report/eventlog.py` — pywin32 `win32evtlog` usage; map severities; source
|
||||
registration via `install` step.
|
||||
3. `report/http.py` — optional webhook with timeout + simple retry/drop on failure
|
||||
4. `report/http.py` — optional webhook with timeout + simple retry/drop on failure
|
||||
(never blocks the poll loop; queue with bound).
|
||||
4. Heartbeat assembly (D15): uptime, reconnects, RTT percentiles, tags-bad counts.
|
||||
5. Heartbeat assembly (D15): uptime, reconnects, RTT percentiles, tags-bad counts.
|
||||
|
||||
**Exit:** events visible in file + Event Log; heartbeat cadence verified.
|
||||
|
||||
@@ -150,6 +174,7 @@ Scenario matrix against a simulation server (and, when available, the real serve
|
||||
| Quality oscillation | toggling sim items | D11 flapping |
|
||||
| Stale/frozen tag | simulation server static item marked `expect_change` | D12 / D13 |
|
||||
| Mass bad quality | all sim items to bad | D14 |
|
||||
| **Device link down** | indicator tag flipped to `false` / set BAD (alias a bool sim item per device, e.g. `P5001.Connected` on Matrikon) | D16 `DEVICE_LINK_DOWN`, recovery line when restored |
|
||||
| Config errors | bad ProgID, unknown tag | fast fail + actionable message; `OPC_E_INVALIDITEMID` surfaced at startup |
|
||||
| Soak | 24 h healthy run | no leaks, heartbeats every 5 min, zero false positives |
|
||||
|
||||
|
||||
@@ -99,9 +99,44 @@ diagnosis.
|
||||
| D13 | Frozen value | `expect_change: true` tag with unchanged value+timestamp for `max_age_s` | WARN |
|
||||
| D14 | Mass quality degradation | ≥ 50% of monitored tags BAD simultaneously while connection OK | CRITICAL (`CONTROL_LINK_DEGRADED` — points at OPC-server↔device link, not DCOM) |
|
||||
| D15 | Heartbeat / summary | Periodic stats snapshot every `heartbeat_s` (300) | INFO |
|
||||
| D16 | Device link down | A configured device-indicator tag reads `false`, or any quality other than GOOD, or a read error | CRITICAL per device (`DEVICE_LINK_DOWN`); D1/D2 do **not** imply device-link state |
|
||||
|
||||
All thresholds are configurable; severities are INFO / WARN / CRITICAL.
|
||||
|
||||
### 6.1 Device connection-indicator tags (D16)
|
||||
|
||||
The config names one or more tags as **device-link indicators**: values the
|
||||
control system maintains that say whether the OPC server can reach the device.
|
||||
The **normal (connected) reading is configurable per indicator** — conventions
|
||||
differ per control system, so it is never hardcoded:
|
||||
|
||||
```yaml
|
||||
devices:
|
||||
- { name: "Conveyor 3", tag: "...Connected" } # any truthy: true/1
|
||||
- { name: "Pump A", tag: "...PumpAFault", ok_values: [false, 0] } # inverted fault flag
|
||||
- { name: "Boiler 2", tag: "...State", ok_values: ["RUN"] } # enum/string state
|
||||
```
|
||||
|
||||
Default `ok_values: [true]` (boolean connected-flags are the common case).
|
||||
Value comparison is by **string equality** on the read value rendered as text
|
||||
(`"True"`, `"1"`, `"RUN"`), so type quirks across systems cannot cause false
|
||||
positives; quality must also be GOOD. Semantics (all DOWN cases are logged as
|
||||
`DEVICE_LINK_DOWN`):
|
||||
|
||||
| Indicator reading | Interpretation |
|
||||
|-------------------|------------------|
|
||||
| GOOD + value ∈ `ok_values` | Device connected — normal |
|
||||
| GOOD + value ∉ `ok_values` | **Device link down / fault** — CRITICAL (works for inverted fault-flags: a fault `1` is not in `[false, 0]`) |
|
||||
| Any quality other than GOOD (BAD/UNCERTAIN) | **Something is wrong with the device link** — CRITICAL |
|
||||
| Read error (tag unreadable) | **Something is wrong with the device link** — CRITICAL |
|
||||
|
||||
Device-link state is per indicator tag: the config maps each indicator to a
|
||||
human-readable device name for the log lines (see `config.example.yaml`).
|
||||
Recovery (`DEVICE_LINK_UP`) is logged when the same indicator reads GOOD with
|
||||
an ok value again. A value that is neither in `ok_values` nor a recognized
|
||||
fault shape still logs DOWN — the normal state is explicit, and everything
|
||||
else is "not normal" by definition.
|
||||
|
||||
## 7. OPC DA quality codes (reference)
|
||||
|
||||
OPC DA quality is a 16-bit field. Bits 7–6 of the low byte give the class:
|
||||
@@ -126,6 +161,13 @@ decodes and names:
|
||||
Remaining UNCERTAIN subcodes (DA 2.05a) are decoded by the same table in `quality.py`.
|
||||
Every quality event carries the raw hex code **and** the decoded name.
|
||||
|
||||
**Clear-text status rule:** wherever a status or quality is reported — JSONL
|
||||
fields, the text log, Event Log, console — it is written in clear text with
|
||||
the raw code in parentheses: `BAD / communication failure (0x18)`,
|
||||
`GOOD (0xC0)`. Nobody should have to decode hex codes by hand, but the raw
|
||||
code stays for unambiguous correlation with vendor documentation and the
|
||||
HRESULT table.
|
||||
|
||||
## 8. Event model
|
||||
|
||||
Every event is a structured record:
|
||||
@@ -163,12 +205,36 @@ alongside and clearly labeled.
|
||||
| Reporter | Purpose | Notes |
|
||||
|----------|---------|-------|
|
||||
| JSONL rotating file | Primary evidence trail | One JSON event per line; rotation by size; default dir `%ProgramData%\OPCDA-Diagnozer\logs` |
|
||||
| **Plain-text anomaly log** | Human-readable evidence | One line per **detected anomaly** (not heartbeats): timestamp, kind, tag/device, message with clear-text status `NAME (0xNN)`; same rotation scheme as JSONL. See §9.1 for the line format |
|
||||
| Windows Event Log | Ops visibility | WARN+ only; source `OPCDA-Diagnozer` |
|
||||
| Console | Foreground/debug runs | Human-readable |
|
||||
| HTTP webhook (optional) | Alert integration | POST one JSON event per request, optional bearer token |
|
||||
|
||||
Reporters are pluggable (`Reporter.emit(event)`); a heartbeat event every 5 minutes
|
||||
proves liveness and enables baseline graphs even when everything is healthy.
|
||||
The JSONL file stays the machine-readable source of truth; the text log is a
|
||||
human-first convenience view of the same events — every text line corresponds
|
||||
to a JSONL record.
|
||||
|
||||
### 9.1 Plain-text anomaly log format
|
||||
|
||||
File `anomalies-YYYYMMDD.log` (daily rotation, size-capped) in the same log
|
||||
directory. One line per detected anomaly — anomalies only; heartbeats and
|
||||
reconnect-OK events do not appear here. Clear text first, codes in parentheses:
|
||||
|
||||
```
|
||||
2026-10-07 14:23:01.123 UTC WARN QUALITY_BAD tag 'Channel1.Device1.Counter' changed GOOD (0xC0) -> BAD / communication failure (0x18)
|
||||
2026-10-07 14:24:31.500 UTC CRITICAL DEVICE_LINK_DOWN device 'Conveyor 3': link indicator 'Channel1.Device3.Connected' read false (GOOD)
|
||||
2026-10-07 14:26:02.780 UTC CRITICAL DEVICE_LINK_DOWN device 'Conveyor 3': link indicator 'Channel1.Device3.Connected' quality BAD / not connected (0x08)
|
||||
2026-10-07 14:27:10.001 UTC CRITICAL POLL_HANG read call hung > 5000 ms; COM thread abandoned and rebuilt
|
||||
2026-10-07 14:30:00.000 UTC CRITICAL CONTROL_LINK_DEGRADED 12 of 14 monitored tags BAD while connection OK
|
||||
```
|
||||
|
||||
Fields (pipe-free, fixed order): `timestamp SEVERITY KIND message`.
|
||||
Timestamps are the local receive time in UTC (§8). The message carries the
|
||||
clear-text status with the code between parentheses per §7. A matching
|
||||
recovery (link back up, quality back to GOOD) is also logged as an anomaly
|
||||
line, so the log reads as a continuous incident narrative.
|
||||
|
||||
## 10. Operational behavior
|
||||
|
||||
|
||||
Reference in New Issue
Block a user