# Log Events

The stable catalog of every `mbproxy.*` event name the service emits, with its level, structured properties, and operational meaning. Operators grep the rolling log against these names, dashboards filter on them, and alerting rules trigger on them — once shipped, the names do not churn.

## Conventions and Wiring

The service uses [Serilog](https://serilog.net/) wired through the `Microsoft.Extensions.Logging` bridge. Three sinks are configured (see `src/Mbproxy/HostingExtensions.cs`):

- **Console** — stdout; captured by the Windows SCM or by systemd-journald.
- **Rolling file** — `%ProgramData%\mbproxy\logs\` on Windows, `/var/log/mbproxy/` on Linux (`mbproxy-<date>.log`).
- **Platform diagnostic sink** — `Error`+ events only. `DiagnosticSinkSelector` picks it once at the composition root: the **Windows Application Event Log** under the SCM (`EventLogBridge`), **local syslog** under systemd (`SyslogBridge`), or none for interactive/dev runs.

Every event uses source-generated `[LoggerMessage]` definitions, so the property names below match the message template token-for-token. The default minimum level is `Information`; lower the floor for `Mbproxy.*` categories via the standard `Logging:LogLevel` configuration to surface `Debug` events such as the coalesce and cache traces.

```json
{
  "Logging": {
    "LogLevel": {
      "Mbproxy": "Debug"
    }
  }
}
```

Every event carries a `Plc` property (the configured PLC name from `appsettings.json`) wherever a PLC scope applies, so log lines for one device can be filtered out of the fleet stream:

```bash
grep '"Plc":"Line1-Mixer"' mbproxy-20260514.log
```

Each H3 below is a stable event identifier; the dotted lowercase casing is part of the operator contract and is preserved verbatim. The `EventId` numeric column is documented alongside the event name so Windows Event Viewer filters and Serilog `EventId`-based subscriptions remain stable too.

## Service Lifecycle

### mbproxy.startup.ready

**Level:** Information &middot; **EventId:** 1 &middot; **Source:** `src/Mbproxy/Proxy/ProxyWorker.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `ListenersBound` | `int` | Number of per-PLC `TcpListener` instances successfully bound at startup. |
| `PlcsConfigured` | `int` | Total PLC entries in the current configuration snapshot. |

Fires once after `ProxyWorker.StartAsync` has spun up every per-PLC supervisor and the admin endpoint. `ListenersBound < PlcsConfigured` means at least one PLC failed its initial bind — the supervisor will keep retrying, but the gap is the operator's first signal.

**Operator action:** if the two counts disagree, search for `mbproxy.startup.bind.failed` entries to identify the missing PLCs.

### mbproxy.startup.bind

**Level:** Information &middot; **EventId:** 20 (`PlcListener`) / 40 (`PlcListenerSupervisor`) &middot; **Source:** `src/Mbproxy/Proxy/PlcListener.cs`, `src/Mbproxy/Proxy/Supervision/PlcListenerSupervisor.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Port` | `int` | Local TCP port the listener is bound to. |

Fires when a per-PLC `TcpListener` successfully binds its configured port. Emitted by both the listener itself and the supervisor wrapper — the two sites share the event name so dashboards filtering on `mbproxy.startup.bind` see both startup binds and post-recovery rebinds.

**Operator action:** none in steady state. Use this event to confirm a hot-reload added PLC actually came up.

### mbproxy.startup.bind.failed

**Level:** Error &middot; **EventId:** 21 (`ProxyWorker`) / 41 (`PlcListenerSupervisor`) &middot; **Source:** `src/Mbproxy/Proxy/ProxyWorker.cs`, `src/Mbproxy/Proxy/Supervision/PlcListenerSupervisor.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Port` | `int` | Port that failed to bind. |
| `Reason` | `string` | Bind exception message (usually `Address already in use` or a permissions error). |

Fires when a listener fails to bind at process startup or after a configuration reload. The supervisor's recovery pipeline will keep retrying with the policy from `Resilience:ListenerRecovery`, so a single occurrence is not necessarily fatal.

**Operator action:** check for port collisions (`netstat -ano | findstr :<port>`). If the conflict is another `mbproxy` instance from a botched uninstall, stop the stray process; if it is a third-party service, change the PLC's `Port` in `appsettings.json` and let hot-reload pick up the change.

### mbproxy.listener.recovered

**Level:** Information &middot; **EventId:** 42 &middot; **Source:** `src/Mbproxy/Proxy/Supervision/PlcListenerSupervisor.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Port` | `int` | Port now bound. |
| `AttemptCount` | `int` | Number of retry attempts the recovery pipeline executed before success. |

Fires after the supervisor's Polly recovery pipeline successfully rebinds a listener that previously emitted `mbproxy.listener.faulted` or `mbproxy.listener.ended`. `AttemptCount` is cumulative for the current outage — useful for spotting a port that is repeatedly flapping.

**Operator action:** none directly. Correlate with the immediately preceding fault to characterise the outage cause.

### mbproxy.listener.faulted

**Level:** Error (`PlcListener`) / Warning (`PlcListenerSupervisor`) &middot; **EventId:** 22 / 43 &middot; **Source:** `src/Mbproxy/Proxy/PlcListener.cs`, `src/Mbproxy/Proxy/Supervision/PlcListenerSupervisor.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Port` | `int` | Port whose listener faulted. |
| `Reason` | `string` | Top-level exception message. |

Fires when a listener's accept loop throws. The two sources emit at different levels deliberately: the unsupervised `PlcListener` instance logs at `Error` (a terminal condition for that listener), while the supervised emission is `Warning` because Polly will retry. The supervised path attaches the exception object as the `LoggerMessage` exception parameter, so the stack trace is captured.

**Operator action:** if the same `Plc` produces repeated faults inside a few minutes, inspect the network path. A burst of faults paired with `mbproxy.multiplex.backend.disconnected` indicates the PLC itself is unhealthy rather than a proxy issue.

### mbproxy.listener.ended

**Level:** Warning &middot; **EventId:** 44 &middot; **Source:** `src/Mbproxy/Proxy/Supervision/PlcListenerSupervisor.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Port` | `int` | Port whose accept loop terminated. |

Fires when an accept loop returns without throwing — usually because the underlying `TcpListener` was stopped without the supervisor requesting it. Treated as a fault: the recovery pipeline rebinds and a subsequent `mbproxy.listener.recovered` should follow.

**Operator action:** none unless paired with no recovery within the configured retry window.

### mbproxy.admin.started

**Level:** Information &middot; **EventId:** 70 &middot; **Source:** `src/Mbproxy/Admin/AdminEndpointHost.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Port` | `int` | TCP port the read-only admin endpoint is listening on. |

Fires when the Kestrel-hosted admin endpoint has bound and is ready to serve `GET /` and `GET /status.json`.

**Operator action:** none. Use this to confirm the configured `AdminPort` is actually serving — `curl http://localhost:<port>/status.json` should return immediately afterwards.

### mbproxy.admin.bind.failed

**Level:** Error &middot; **EventId:** 71 &middot; **Source:** `src/Mbproxy/Admin/AdminEndpointHost.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Port` | `int` | Port that failed to bind. |
| `Reason` | `string` | Bind exception message. |

Fires when the admin endpoint cannot bind its configured `AdminPort`. The service continues to proxy Modbus traffic — only the status page and `status.json` are unavailable.

**Operator action:** change `Mbproxy:AdminPort` in `appsettings.json` to a free port. Hot-reload picks up the change; the admin endpoint rebinds without a service restart.

### mbproxy.shutdown.complete

**Level:** Information &middot; **EventId:** 80 &middot; **Source:** `src/Mbproxy/Diagnostics/ShutdownCoordinator.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `InFlightAtCancel` | `int` | Aggregate in-flight request count across all multiplexers at the moment SIGTERM was received. |
| `ElapsedMs` | `long` | Wall-clock time spent draining, capped by `Connection:GracefulShutdownTimeoutMs`. |

Fires once after the graceful drain completes (whether all in-flight requests finished or the timeout fired first). `ElapsedMs` close to the configured drain timeout indicates the drain budget was exhausted — some upstream clients saw forced disconnects.

**Operator action:** if `InFlightAtCancel` is consistently large, consider raising `Connection:GracefulShutdownTimeoutMs` so restarts don't strand in-flight reads/writes.

## Client Sessions

### mbproxy.client.connected

**Level:** Information &middot; **EventId:** 110 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `RemoteEp` | `string` | `IPAddress:port` of the upstream client. |

Fires once per upstream client `accept` on a PLC's listener. The event name is preserved from the legacy 1:1 model so existing operator queries keep working after the multiplexer rewrite.

**Operator action:** none in steady state. A burst of connects from the same `RemoteEp` indicates a reconnect storm — pair with `mbproxy.client.disconnected` to confirm.

### mbproxy.client.disconnected

**Level:** Information &middot; **EventId:** 111 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `RemoteEp` | `string` | `IPAddress:port` of the upstream client. |
| `Reason` | `string` | Disconnect reason: `clean`, an exception type, or `cascade` when the multiplexer closed the upstream pipe due to a backend failure. |

Fires when an upstream pipe is closed for any reason.

**Operator action:** none unless paired with `mbproxy.multiplex.backend.disconnected` — that combination indicates the disconnect cascade is fleet-wide for one PLC.

## Backend Multiplexer

### mbproxy.multiplex.backend.connected

**Level:** Information &middot; **EventId:** 112 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Host` | `string` | Backend host or IP. |
| `Port` | `int` | Backend TCP port (typically 502). |

Fires when the multiplexer's single backend socket to a PLC is established. Because the multiplexer holds exactly one backend socket per PLC and reuses it across every upstream client, this event is rare in steady state — see it at startup and after each `mbproxy.multiplex.backend.disconnected`.

**Operator action:** none.

### mbproxy.multiplex.backend.disconnected

**Level:** Warning &middot; **EventId:** 113 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UpstreamCount` | `int` | Number of upstream pipes the multiplexer cascade-closed. |
| `InFlightCount` | `int` | Number of in-flight requests dropped (each upstream sees a `Gateway Target Device Failed To Respond` exception or a closed socket). |
| `Reason` | `string` | Underlying disconnect reason (exception type or `"backend EOF"`). |

Fires when the backend socket closes for any reason. Closing the backend is fatal to all attached upstream pipes; the multiplexer cascade-closes them so clients reconnect cleanly through the listener.

**Operator action:** investigate the PLC and the network path. A single transient occurrence is normal — repeated occurrences for the same `Plc` indicate a sick controller, a flaky ECOM100, or an unstable middlebox. Pair with `mbproxy.backend.failed` events for the same PLC to confirm the proxy can't get back in.

### mbproxy.multiplex.saturated

**Level:** Error &middot; **EventId:** 114 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `RemoteEp` | `string` | Upstream client whose request was refused. |

Fires when the TxId allocator refuses to allocate — every slot in the 16-bit MBAP transaction-ID space is currently in flight. The multiplexer responds to the upstream with Modbus exception code 04 (`Slave Device Failure`) and frees nothing. The DL205/DL260 family serialises Modbus TCP at roughly 2–10 ms per request, so reaching 65 536 concurrent in-flight requests is a stress-only path; in production this event is alert-worthy because it indicates either a runaway client or a backend that has stopped responding.

**Operator action:** alert. Check the corresponding PLC's in-flight gauge on the status page; if it is also pegged, the backend is wedged and the listener should be restarted via the hot-reload path (toggle the PLC's `Enabled` flag).

### mbproxy.multiplex.request.timeout

**Level:** Warning &middot; **EventId:** 116 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `ProxyTxId` | `ushort` | Internal TxId the multiplexer assigned to the backend request. |
| `OriginalTxId` | `ushort` | TxId the upstream client sent. |
| `Fc` | `byte` | Modbus function code of the timed-out request. |
| `ElapsedMs` | `long` | Wall-clock time the request spent in flight before the watchdog fired. |

Fires when the per-request watchdog times out an in-flight request whose response never arrived within `BackendRequestTimeoutMs`. The upstream client receives Modbus exception code `0x0B` (`Gateway Target Device Failed To Respond`) and the proxy TxId is freed. Common causes: PLC dropped the response, packet loss, or a backend that echoes the wrong MBAP TxId (e.g. pymodbus 3.13.0's concurrent-multiplexed-request bug).

**Operator action:** isolated timeouts are noise. A sustained rate indicates the PLC is overloaded or the backend is misbehaving — correlate with `mbproxy.multiplex.backend.disconnected`.

### mbproxy.backend.failed

**Level:** Warning &middot; **EventId:** 115 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Reason` | `string` | Final connect-pipeline exception (after retries). |

Fires when the backend-connect Polly pipeline (3 attempts at 100 ms / 500 ms / 2000 ms by default) exhausts its retries. The multiplexer cascade-closes any waiting upstream pipes; new clients will trigger a fresh reconnect attempt.

**Operator action:** check the PLC's IP/port reachability. If multiple PLCs share an upstream switch or an EBC100 daughterboard, look for a common network event.

## Read Coalescing

### mbproxy.coalesce.hit

**Level:** Debug &middot; **EventId:** 120 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/CoalescingLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UnitId` | `byte` | Modbus unit identifier from the request MBAP. |
| `Fc` | `byte` | Function code (`0x03` or `0x04`). |
| `Start` | `ushort` | First register in the request range. |
| `Qty` | `ushort` | Quantity of registers. |
| `PartyCount` | `int` | Number of upstream parties now attached to the in-flight peer (after this one joined). |

Fires when an FC03/FC04 request attaches to an in-flight request with the same `(UnitId, Fc, Start, Qty)` key, so only one wire request hits the PLC.

**Operator action:** none. Coalescing is steady-state behaviour; the counters on the status page surface the same data without log volume.

### mbproxy.coalesce.miss

**Level:** Debug &middot; **EventId:** 121 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/CoalescingLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UnitId` | `byte` | Modbus unit identifier. |
| `Fc` | `byte` | Function code (`0x03` or `0x04`). |
| `Start` | `ushort` | First register in the request range. |
| `Qty` | `ushort` | Quantity of registers. |

Fires when an FC03/FC04 request opens a fresh in-flight entry — either no matching peer existed, or the matching peer had reached its `MaxParties` cap and a new entry was opened.

**Operator action:** none.

### mbproxy.coalesce.dead_upstream

**Level:** Debug &middot; **EventId:** 122 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/CoalescingLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UnitId` | `byte` | Modbus unit identifier. |
| `Fc` | `byte` | Function code (`0x03` or `0x04`). |
| `Start` | `ushort` | First register in the request range. |
| `Qty` | `ushort` | Quantity of registers. |

Fires when fan-out skips a coalesced party because its upstream pipe was closed (or had pending writes the pipe rejected). A high rate paired with a high `mbproxy.client.disconnected` rate indicates clients are giving up on slow PLCs.

**Operator action:** none directly. Investigate if paired with rising `mbproxy.multiplex.request.timeout` on the same PLC.

## Response Cache

### mbproxy.cache.hit

**Level:** Debug &middot; **EventId:** 140 &middot; **Source:** `src/Mbproxy/Proxy/Cache/CacheLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UnitId` | `byte` | Modbus unit identifier. |
| `Fc` | `byte` | Function code (`0x03` or `0x04`). |
| `Start` | `ushort` | First register in the request range. |
| `Qty` | `ushort` | Quantity of registers. |

Fires when an FC03/FC04 request is served entirely from the in-process response cache — no backend round-trip occurs and the result is rewritten through the BCD pipeline as usual.

**Operator action:** none. The status-page cache hit-rate gauge tracks the same signal at far lower cost than this event.

### mbproxy.cache.miss

**Level:** Debug &middot; **EventId:** 141 &middot; **Source:** `src/Mbproxy/Proxy/Cache/CacheLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UnitId` | `byte` | Modbus unit identifier. |
| `Fc` | `byte` | Function code. |
| `Start` | `ushort` | First register in the request range. |
| `Qty` | `ushort` | Quantity of registers. |

Fires when an FC03/FC04 request matches a cacheable tag range but no live cache entry exists; the request falls through to the backend.

**Operator action:** none.

### mbproxy.cache.store

**Level:** Debug &middot; **EventId:** 142 &middot; **Source:** `src/Mbproxy/Proxy/Cache/CacheLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UnitId` | `byte` | Modbus unit identifier. |
| `Fc` | `byte` | Function code. |
| `Start` | `ushort` | First register in the request range. |
| `Qty` | `ushort` | Quantity of registers. |
| `TtlMs` | `int` | Cache TTL applied to the entry (in milliseconds). |

Fires when a successful FC03/FC04 response is admitted into the cache. `TtlMs` echoes the per-tag `CacheTtlMs` from `appsettings.json`.

**Operator action:** none.

### mbproxy.cache.invalidated

**Level:** Debug &middot; **EventId:** 143 &middot; **Source:** `src/Mbproxy/Proxy/Cache/CacheLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `UnitId` | `byte` | Modbus unit identifier. |
| `WriteStart` | `ushort` | First register the write touched. |
| `WriteQty` | `ushort` | Number of registers the write touched. |
| `Count` | `int` | Number of cache entries evicted by the write. |

Fires when an FC06/FC16 write invalidates one or more overlapping cache entries. The invalidator deliberately operates at the register level so a write to `V2000` evicts every cached read range that includes register `V2000`.

**Operator action:** none. A `Count` of zero means the write was outside any cached range and is informational only.

### mbproxy.cache.flushed

**Level:** Information &middot; **EventId:** 144 &middot; **Source:** `src/Mbproxy/Proxy/Cache/CacheLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Reason` | `string` | Why the cache was flushed: `"backend reconnect"`, `"hot-reload"`, or `"shutdown"`. |
| `Count` | `int` | Number of entries dropped. |

Fires whenever the entire per-PLC cache is wiped at once — primarily after a backend reconnect (the proxy can't reason about staleness across the gap) or after a hot-reload that changed the BCD tag map for the PLC.

**Operator action:** none unless flushes happen on a tight loop, which would indicate the backend connection itself is unstable.

## Keepalive

See [`../Architecture/Keepalive.md`](../Architecture/Keepalive.md) for the backend heartbeat design.

### mbproxy.keepalive.heartbeat.sent

**Level:** Debug &middot; **EventId:** 150 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/KeepaliveLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `ProxyTxId` | `ushort` | Proxy-allocated TxId carrying the synthetic FC03 probe. |
| `Address` | `ushort` | Modbus address the probe reads (`BackendHeartbeatProbeAddress`). |

Fires each time the heartbeat loop issues a probe on an idle backend socket — at most one per `BackendHeartbeatIdleMs` per idle PLC.

**Operator action:** none. Debug-level; useful only when confirming the heartbeat is alive.

### mbproxy.keepalive.heartbeat.timeout

**Level:** Warning &middot; **EventId:** 151 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/KeepaliveLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `ProxyTxId` | `ushort` | Proxy TxId of the unanswered probe. |
| `ElapsedMs` | `long` | Milliseconds from probe send to timeout. |

Fires when a heartbeat probe is not answered within `BackendRequestTimeoutMs` — the backend is connected but no longer answering Modbus.

**Operator action:** check the PLC and the network path. Paired with `mbproxy.keepalive.backend.idle_disconnect` for the same PLC.

### mbproxy.keepalive.backend.idle_disconnect

**Level:** Information &middot; **EventId:** 152 &middot; **Source:** `src/Mbproxy/Proxy/Multiplexing/KeepaliveLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `ElapsedMs` | `long` | Milliseconds the failed heartbeat waited before the teardown. |

Fires when a failed heartbeat triggers a proactive backend teardown. Every attached upstream pipe is cascaded; clients reconnect on their next request. This is the keepalive feature doing its job — finding a dead path during idle instead of on the next real request.

**Operator action:** none if isolated. Repeated idle-disconnects on one PLC indicate it keeps going dark while idle — investigate the device or the network path.

## BCD Rewriter

### mbproxy.rewrite.partial_bcd

**Level:** Warning &middot; **EventId:** 30 &middot; **Source:** `src/Mbproxy/Proxy/RewriterLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Address` | `ushort` | PDU-decimal address of the BCD tag involved. |
| `ClientStart` | `ushort` | Start register the client requested. |
| `ClientQty` | `ushort` | Quantity of registers the client requested. |

Fires when a 32-bit BCD pair is only partially covered by a read or write range (the request straddles the CDAB boundary instead of covering both words). The raw bytes are passed through unchanged, so the client or PLC sees the original nibbles.

**Operator action:** the upstream client is misconfigured — its register map disagrees with the proxy's `BcdTags` list. Reconcile the client's tag definitions against `appsettings.json`.

### mbproxy.rewrite.invalid_bcd

**Level:** Warning &middot; **EventId:** 31 &middot; **Source:** `src/Mbproxy/Proxy/RewriterLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Address` | `ushort` | PDU-decimal address of the BCD tag involved. |
| `RawValue` | `ushort` | The raw register value that failed BCD validation (logged as `0x{RawValue:X4}`). |
| `Direction` | `string` | `"Read"` (response from PLC) or `"Write"` (request from client). |

Fires when a register at a configured BCD address contains a nibble `>= 0xA` — i.e. not a valid BCD digit. The raw bytes are passed through unchanged.

**Operator action:** if `Direction=Read`, the PLC has been written outside BCD discipline (probably by a ladder program that bypassed the proxy). If `Direction=Write`, the client is sending a value `> 9999` that doesn't fit in 4 BCD digits — bound-check upstream.

### mbproxy.exception.passthrough

**Level:** Information &middot; **EventId:** 32 &middot; **Source:** `src/Mbproxy/Proxy/RewriterLogEvents.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Plc` | `string` | Configured PLC name. |
| `Fc` | `byte` | Original function code (high bit set on the wire; logged as `0x{Fc:X2}`). |
| `ExceptionCode` | `byte` | Modbus exception code (1=illegal function, 2=illegal data address, 3=illegal data value, 4=slave device failure, etc.). |

Fires when the PLC returns a Modbus exception response (high bit set on the FC byte). The frame is forwarded verbatim to the client.

**Operator action:** none in isolation. A sustained `ExceptionCode=2` rate against a configured BCD address suggests the PLC's V-memory map no longer matches the proxy's tag list.

## Configuration Hot-Reload

### mbproxy.config.reload.applied

**Level:** Information &middot; **EventId:** 60 &middot; **Source:** `src/Mbproxy/Configuration/ConfigReconciler.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `PlcsAdded` | `int` | Number of new PLC entries the supervisor brought online. |
| `PlcsRemoved` | `int` | Number of PLC entries torn down. |
| `PlcsRestarted` | `int` | Number of PLCs whose listener was rebound (port or host change). |
| `PlcsReseated` | `int` | Number of PLCs whose BCD tag map was swapped without restarting the listener. |
| `GlobalTagDelta` | `int` | Net change in the global tag count across the new snapshot. |

Fires after a debounced `appsettings.json` change passes validation and the reconciler has applied it. The five counters together describe the shape of the change so dashboards can plot churn over time.

**Operator action:** none. Use this event as the audit trail for who-changed-what after a configuration push.

### mbproxy.config.reload.rejected

**Level:** Error &middot; **EventId:** 61 &middot; **Source:** `src/Mbproxy/Configuration/ConfigReconciler.cs`

| Property | Type | Meaning |
|----------|------|---------|
| `Errors` | `string` | Concatenated validation failures (one line per failure). |

Fires when a configuration change fails validation — duplicate PLC names, port collisions inside the new file, malformed BCD tag entries, or a schema-level error. The previous valid configuration remains in effect; no listeners are touched.

**Operator action:** fix the offending entry in `appsettings.json` and save again. The reconciler debounces file events on a 250 ms window, so rapid sequential saves coalesce into one validation pass.

## Conventions

### Naming

All event names follow `mbproxy.<area>.<noun>[.<state>]`:

- `<area>` matches a subsystem (`startup`, `client`, `multiplex`, `coalesce`, `cache`, `rewrite`, `config`, `admin`, `listener`, `shutdown`, `backend`, `exception`).
- `<noun>` describes the thing the event is about.
- `<state>` is optional and only used when the event represents a terminal or special outcome (`bind.failed`, `reload.rejected`, `backend.connected`).

Property names are PascalCase, match the `[LoggerMessage]` template tokens exactly, and use `Plc` as the canonical scope key for per-device filtering.

### Stability Promise

Event names and `EventId` values are part of the operator contract. They are not changed in patch or minor releases. A rename or removal requires a major version bump and a migration note in the release. New events are additive and can ship in any release.

### Where Events Are Defined

Each subsystem owns a single `*LogEvents.cs` static partial class with `[LoggerMessage]` declarations:

- `src/Mbproxy/Proxy/Multiplexing/MultiplexerLogEvents.cs` — client sessions and backend multiplexer.
- `src/Mbproxy/Proxy/Multiplexing/CoalescingLogEvents.cs` — read coalescing.
- `src/Mbproxy/Proxy/Cache/CacheLogEvents.cs` — response cache.
- `src/Mbproxy/Proxy/RewriterLogEvents.cs` — BCD rewriting and exception passthrough.

Lifecycle events (`startup.*`, `listener.*`, `admin.*`, `shutdown.*`, `config.reload.*`) live as private `[LoggerMessage]` declarations next to the class that emits them — see `ProxyWorker.cs`, `PlcListener.cs`, `PlcListenerSupervisor.cs`, `AdminEndpointHost.cs`, `ShutdownCoordinator.cs`, and `ConfigReconciler.cs`. New subsystems should follow the `*LogEvents.cs` pattern when they accumulate more than two events.

## Related Documentation

- [Troubleshooting](../Operations/Troubleshooting.md) — operator playbook keyed off the event names in this catalog.
- [Connection Model](../Architecture/ConnectionModel.md) — context for the `mbproxy.multiplex.*` and `mbproxy.client.*` events.
- [Response Cache](../Architecture/ResponseCache.md) — context for the `mbproxy.cache.*` events.
- [Status Page](../Operations/StatusPage.md) — counter equivalents for the high-volume Debug-level events.
- [Read Coalescing](../Architecture/ReadCoalescing.md) — context for the `mbproxy.coalesce.*` events.
- [Keepalive](../Architecture/Keepalive.md) — context for the `mbproxy.keepalive.*` events.
- [BCD Rewriting](../Features/BcdRewriting.md) — context for the `mbproxy.rewrite.*` and `mbproxy.exception.passthrough` events.
- [Hot Reload](../Features/HotReload.md) — context for the `mbproxy.config.reload.*` events.
