Scope alarm tracking to selected templates and surface endpoint/security state on the dashboard so operators can deploy in large galaxies without drowning clients in irrelevant alarms or guessing what the server is advertising
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,70 @@ The `IsAlarm` flag originates from the `AlarmExtension` primitive in the Galaxy
|
||||
|
||||
For each alarm attribute, the code verifies that a corresponding `InAlarm` sub-attribute variable node exists in `_tagToVariableNode` (constructed from `FullTagReference + ".InAlarm"`). If the variable node is missing, the alarm is skipped -- this prevents creating orphaned alarm conditions for attributes whose extension primitives were not published.
|
||||
|
||||
## Template-Based Alarm Object Filter
|
||||
|
||||
When large galaxies contain more alarm-bearing objects than clients need, `OpcUa.AlarmFilter.ObjectFilters` restricts alarm condition creation to a subset of objects selected by **template name pattern**. The filter is applied at both alarm creation sites -- the full build in `BuildAddressSpace` and the subtree rebuild path triggered by Galaxy redeployment -- so the included set is recomputed on every rebuild against the fresh hierarchy.
|
||||
|
||||
### Matching rules
|
||||
|
||||
- `*` is the only wildcard (glob-style, zero or more characters). All other regex metacharacters are escaped and matched literally.
|
||||
- Matching is case-insensitive.
|
||||
- The leading `$` used by Galaxy template `tag_name` values is normalized away on both the stored chain entry and the operator pattern, so `TestMachine*` matches the stored `$TestMachine`.
|
||||
- Each configured entry may itself be comma-separated for operator convenience (`"TestMachine*, Pump_*"`).
|
||||
- An empty list disables the filter and restores the prior behavior: every alarm-bearing object is tracked when `AlarmTrackingEnabled=true`.
|
||||
|
||||
### What gets included
|
||||
|
||||
Every Galaxy object whose **template derivation chain** contains any template matching any pattern is included. The chain walks `gobject.derived_from_gobject_id` from the instance through its immediate template and each ancestor template, up to `$Object`. An instance of `TestCoolMachine` whose chain is `$TestCoolMachine -> $TestMachine -> $UserDefined` matches the pattern `TestMachine` via the ancestor hit.
|
||||
|
||||
Inclusion propagates down the **containment hierarchy**: if an object matches, all of its descendants are included as well, regardless of their own template chains. This lets operators target a parent and pick up all its alarm-bearing children with one pattern.
|
||||
|
||||
Each object is evaluated exactly once. Overlapping matches (multiple patterns hit, or both an ancestor and descendant match independently) never produce duplicate alarm condition subscriptions -- the filter operates on object identity via a `HashSet<int>` of included `GobjectId` values.
|
||||
|
||||
### Resolution algorithm
|
||||
|
||||
`AlarmObjectFilter.ResolveIncludedObjects(hierarchy)` runs once per build:
|
||||
|
||||
1. Compile each pattern into a regex with `IgnoreCase | CultureInvariant | Compiled`.
|
||||
2. Build a `parent -> children` map from the hierarchy. Orphans (parent id not in the hierarchy) are treated as roots.
|
||||
3. BFS from each root with a `(nodeId, parentIncluded)` queue and a `visited` set for cycle defense.
|
||||
4. At each node: if the parent was included OR any chain entry matches any pattern, add the node and mark its subtree as included.
|
||||
5. Return the `HashSet<int>` of included object IDs. When no patterns are configured the filter is disabled and the method returns `null`, which the alarm loop treats as "no filtering".
|
||||
|
||||
After each resolution, `UnmatchedPatterns` exposes any raw pattern that matched zero objects so the startup log can warn about operator typos without failing startup.
|
||||
|
||||
### How the alarm loop applies the filter
|
||||
|
||||
```csharp
|
||||
// LmxNodeManager.BuildAddressSpace (and the subtree rebuild path)
|
||||
if (_alarmTrackingEnabled)
|
||||
{
|
||||
var includedIds = ResolveAlarmFilterIncludedIds(sorted); // null if no filter
|
||||
foreach (var obj in sorted)
|
||||
{
|
||||
if (obj.IsArea) continue;
|
||||
if (includedIds != null && !includedIds.Contains(obj.GobjectId)) continue;
|
||||
// ... existing alarm-attribute collection + AlarmConditionState creation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ResolveAlarmFilterIncludedIds` also emits a one-line summary (`Alarm filter: X of Y objects included (Z pattern(s))`) and per-pattern warnings for patterns that matched nothing. The included count is published to the dashboard via `AlarmFilterIncludedObjectCount`.
|
||||
|
||||
### Runtime telemetry
|
||||
|
||||
`LmxNodeManager` exposes three read-only properties populated by the filter:
|
||||
|
||||
- `AlarmFilterEnabled` -- true when patterns are configured.
|
||||
- `AlarmFilterPatternCount` -- number of compiled patterns.
|
||||
- `AlarmFilterIncludedObjectCount` -- number of objects in the most recent included set.
|
||||
|
||||
`StatusReportService` reads these into `AlarmStatusInfo.FilterEnabled`, `FilterPatternCount`, and `FilterIncludedObjectCount`. The Alarms panel on the dashboard renders `Filter: N pattern(s), M object(s) included` only when the filter is enabled. See [Status Dashboard](StatusDashboard.md#alarms).
|
||||
|
||||
### Validator warning
|
||||
|
||||
`ConfigurationValidator.ValidateAndLog()` logs the effective filter at startup and emits a `Warning` if `AlarmFilter.ObjectFilters` is non-empty while `AlarmTrackingEnabled` is `false`, because the filter would have no effect.
|
||||
|
||||
## AlarmConditionState Creation
|
||||
|
||||
Each detected alarm attribute produces an `AlarmConditionState` node:
|
||||
|
||||
@@ -55,6 +55,7 @@ Controls the OPC UA server endpoint and session limits. Defined in `OpcUaConfigu
|
||||
| `MaxSessions` | `int` | `100` | Maximum simultaneous OPC UA sessions |
|
||||
| `SessionTimeoutMinutes` | `int` | `30` | Idle session timeout in minutes |
|
||||
| `AlarmTrackingEnabled` | `bool` | `false` | Enables `AlarmConditionState` nodes for alarm attributes |
|
||||
| `AlarmFilter.ObjectFilters` | `List<string>` | `[]` | Wildcard template-name patterns (with `*`) that scope alarm tracking to matching objects and their descendants. Empty list disables filtering. See [Alarm Tracking](AlarmTracking.md#template-based-alarm-object-filter) |
|
||||
| `ApplicationUri` | `string?` | `null` | Explicit application URI for this server instance. Required when redundancy is enabled. Defaults to `urn:{GalaxyName}:LmxOpcUa` when null |
|
||||
|
||||
### MxAccess
|
||||
@@ -232,6 +233,7 @@ Example — two-instance redundant pair (Primary):
|
||||
Three boolean properties act as feature flags that control optional subsystems:
|
||||
|
||||
- **`OpcUa.AlarmTrackingEnabled`** -- When `true`, the node manager creates `AlarmConditionState` nodes for alarm attributes and monitors `InAlarm` transitions. Disabled by default because alarm tracking adds per-attribute overhead.
|
||||
- **`OpcUa.AlarmFilter.ObjectFilters`** -- List of wildcard template-name patterns that scope alarm tracking to matching objects and their descendants. An empty list preserves the current unfiltered behavior; a non-empty list includes an object only when any name in its template derivation chain matches any pattern, then propagates the inclusion to every descendant in the containment hierarchy. `*` is the only wildcard, matching is case-insensitive, and the Galaxy `$` prefix on template names is normalized so operators can write `TestMachine*` instead of `$TestMachine*`. Each list entry may itself contain comma-separated patterns (`"TestMachine*, Pump_*"`) for convenience. When the list is non-empty but `AlarmTrackingEnabled` is `false`, the validator emits a warning because the filter has no effect. See [Alarm Tracking](AlarmTracking.md#template-based-alarm-object-filter) for the full matching algorithm and telemetry.
|
||||
- **`Historian.Enabled`** -- When `true`, the service calls `HistorianPluginLoader.TryLoad(config)` to load the `ZB.MOM.WW.LmxOpcUa.Historian.Aveva` plugin from the `Historian/` subfolder next to the host exe and registers the resulting `IHistorianDataSource` with the OPC UA server host. Disabled by default because not all deployments have a Historian instance -- when disabled the plugin is not probed and the Wonderware SDK DLLs are not required on the host. If the flag is `true` but the plugin or its SDK dependencies cannot be loaded, the server still starts and every history read returns `BadHistoryOperationUnsupported` with a warning in the log.
|
||||
- **`GalaxyRepository.ExtendedAttributes`** -- When `true`, the repository loads additional Galaxy attribute metadata beyond the core set needed for the address space. Disabled by default to minimize startup query time.
|
||||
|
||||
@@ -247,6 +249,7 @@ Three boolean properties act as feature flags that control optional subsystems:
|
||||
- Unknown security profile names are logged as warnings
|
||||
- `AutoAcceptClientCertificates = true` emits a warning
|
||||
- Only-`None` profile configuration emits a warning
|
||||
- `OpcUa.AlarmFilter.ObjectFilters` is non-empty while `OpcUa.AlarmTrackingEnabled = false` emits a warning (filter has no effect)
|
||||
- `OpcUa.ApplicationUri` must be set when `Redundancy.Enabled = true`
|
||||
- `Redundancy.ServiceLevelBase` must be between 1 and 255
|
||||
- `Redundancy.ServerUris` should contain at least 2 entries when enabled
|
||||
@@ -282,6 +285,9 @@ Integration tests use this constructor to inject substitute implementations of `
|
||||
"MaxSessions": 100,
|
||||
"SessionTimeoutMinutes": 30,
|
||||
"AlarmTrackingEnabled": false,
|
||||
"AlarmFilter": {
|
||||
"ObjectFilters": []
|
||||
},
|
||||
"ApplicationUri": null
|
||||
},
|
||||
"MxAccess": {
|
||||
|
||||
@@ -21,7 +21,7 @@ All queries are embedded as `const string` fields in `GalaxyRepositoryService`.
|
||||
|
||||
### Hierarchy query
|
||||
|
||||
Returns deployed Galaxy objects with their parent relationships and browse names:
|
||||
Returns deployed Galaxy objects with their parent relationships, browse names, and template derivation chains:
|
||||
|
||||
- Joins `gobject` to `template_definition` to filter by relevant `category_id` values (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||
- Uses `contained_name` as the browse name, falling back to `tag_name` when `contained_name` is null or empty
|
||||
@@ -29,6 +29,7 @@ Returns deployed Galaxy objects with their parent relationships and browse names
|
||||
- Marks objects with `category_id = 13` as areas
|
||||
- Filters to `is_template = 0` (instances only, not templates)
|
||||
- Filters to `deployed_package_id <> 0` (deployed objects only)
|
||||
- Returns a `template_chain` column built by a recursive CTE that walks `gobject.derived_from_gobject_id` from each instance through its immediate template and ancestor templates (depth guard `< 10`). Template names are ordered by depth and joined with `|` via `STUFF(... FOR XML PATH(''))`. Example: `TestMachine_001` returns `$TestMachine|$gMachine|$gUserDefined|$UserDefined`. The C# repository reader splits the column on `|`, trims, and populates `GalaxyObjectInfo.TemplateChain`, which is consumed by `AlarmObjectFilter` for template-based alarm filtering. See [Alarm Tracking](AlarmTracking.md#template-based-alarm-object-filter).
|
||||
|
||||
### Attributes query (standard)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ The OPC UA server component hosts the Galaxy-backed namespace on a configurable
|
||||
| `MaxSessions` | `100` | Maximum concurrent client sessions |
|
||||
| `SessionTimeoutMinutes` | `30` | Idle session timeout |
|
||||
| `AlarmTrackingEnabled` | `false` | Enables `AlarmConditionState` nodes for alarm attributes |
|
||||
| `AlarmFilter.ObjectFilters` | `[]` | Wildcard template-name patterns that scope alarm tracking to matching objects and their descendants (see [Alarm Tracking](AlarmTracking.md#template-based-alarm-object-filter)) |
|
||||
|
||||
The resulting endpoint URL is `opc.tcp://{BindAddress}:{Port}{EndpointPath}`, e.g., `opc.tcp://0.0.0.0:4840/LmxOpcUa`.
|
||||
|
||||
|
||||
@@ -127,6 +127,11 @@ New operation names are auto-registered on first use, so the `Operations` dictio
|
||||
| `TransitionCount` | `long` | Total `InAlarm` transitions observed in the dispatch loop since startup |
|
||||
| `AckEventCount` | `long` | Total alarm acknowledgement transitions observed since startup |
|
||||
| `AckWriteFailures` | `long` | Total MXAccess AckMsg writes that have failed while processing alarm acknowledges. Any non-zero value latches the service into Degraded (see Rule 2d). |
|
||||
| `FilterEnabled` | `bool` | Whether `OpcUa.AlarmFilter.ObjectFilters` has any patterns configured |
|
||||
| `FilterPatternCount` | `int` | Number of compiled filter patterns (after comma-splitting and trimming) |
|
||||
| `FilterIncludedObjectCount` | `int` | Number of Galaxy objects included by the filter during the most recent address-space build. Zero when the filter is disabled. |
|
||||
|
||||
When the filter is active, the operator dashboard's Alarms panel renders an extra line `Filter: N pattern(s), M object(s) included` so operators can verify scope at a glance. See [Alarm Tracking](AlarmTracking.md#template-based-alarm-object-filter) for the matching rules and resolution algorithm.
|
||||
|
||||
### Redundancy
|
||||
|
||||
|
||||
Reference in New Issue
Block a user