Your custom Ignition 8.3 driver shows up under Connections → Devices → Connections. You added a View Details menu action pointing at the built-in DeviceDetails React component, and the page it opens is empty or broken. The page runs on data your driver never supplies. Implement DeviceExtensionPoint::getSamplingMetrics, feed it from com.codahale.metrics.Timer instances, and tag your log output with the device-name MDC key.
Drop the fixes that leave the Details page empty
These are the usual attempts. None of them supplies the data the page needs.
-
Wiring a MenuAction to the built-in component. This is the most common attempt:
The menu entry appears, and the React component loads withpublic List<MenuAction> getMenuActions() { return List.of(new MenuAction("View Details", "View device details.", (WebUiComponent) new ReactComponentInfo("DeviceDetails", null))); }nullprops. Nothing on the gateway side hands it any statistics.getMenuActionsis for your own custom links and React pages. It does not register your device with the diagnostics page. -
Emitting JSON that matches the gateway's serializer. The gateway already has a
SamplingMetricsSerializerthat writes anaggregateobject and asamplingGroupsarray. You do not produce that JSON yourself. You return typedSamplingMetricsrecords, and the gateway serializes them. Treat the serializer only as a cross-check on field names. -
Configuring loggers or a minimum level for Log Activity. You cannot do this. The panel shows any log statement whose MDC key
device-nameequals the device instance name. There are no logger selection or level settings to change. -
Searching for the Javadoc. Before the 8.3.0 release, the Javadoc for the
opc-ua-gateway-apimodule was not published. Read the record constructors directly from the API jar in your IDE instead of waiting for docs.
Match what the Details page shows to what the driver is missing
Check first whether getSamplingMetrics is overridden at all. That single method decides whether the statistics sections have anything to render.
| What you see | Cause | Fix |
|---|---|---|
| Custom "View Details" opens a blank or erroring page | Custom MenuAction points at DeviceDetails with null props; no metrics source |
Implement getSamplingMetrics; keep getMenuActions for your own pages only |
| Aggregate Statistics empty or zero |
getSamplingMetrics not overridden, or it returns records built from Timers that never recorded anything |
Time every request execution with a Timer
|
| Aggregate populated, sampling group list empty |
samplingGroupStats list passed empty |
Build one SamplingGroupStats per scheduled group |
| Histogram and throughput stay flat while requests run | Snapshot or rates taken from a Timer the request path does not update | Update the same Timer instance that the metrics builder reads |
| Log Activity empty, although the driver logs | Log calls lack the device-name MDC key, or the key value differs from the device instance name |
Build the logger with LoggerEx.newBuilder().mdcContext("device-name", context.name)
|
| Log Activity shows some driver lines but not others | Those lines come from loggers or threads without the MDC context, such as a plain logger or a third-party library | Route driver logging through the MDC-tagged LoggerEx
|
Know what the gateway reads when it renders Details
The Details page combines two independent feeds:
-
Statistics. These come from the
SamplingMetricsobject returned byDeviceExtensionPoint::getSamplingMetrics. The gateway serializes it into anaggregateblock and asamplingGroupsarray. -
Log Activity. This is a filter over the gateway log stream on MDC key
device-name. Your driver controls only whether its statements carry the key.
The record shapes are fixed:
public record SamplingMetrics(
AggregateStats aggregateStats,
List<SamplingGroupStats> samplingGroupStats
) {
public record AggregateStats(
int requestCount,
int monitoredItemCount,
Snapshot histogram,
double meanThroughput,
double oneMinuteThroughput
) {}
public record SamplingGroupStats(
int requestCount,
int samplingInterval,
double queueDuration,
double overloadFactor,
Snapshot histogram,
double meanThroughput,
double oneMinuteThroughput
) {}
}
The Snapshot objects come from com.codahale.metrics.Timer. The whole sampling-metrics model assumes your driver measures its request executions with a Timer. If your driver has no concept of a timed request, instrument one before you touch the UI.
Instrument each sampling group with a Codahale Timer
- Create one
Timerfor the aggregate and one per sampling group. Keep them in a structure keyed by device instance so the metrics builder and the request path share the same objects. - Wrap every request execution. Record the elapsed time into both the group Timer and the aggregate Timer.
- Track the non-Timer fields in your scheduler: request count per group, monitored item count, queue duration, and overload factor.
- Override
getSamplingMetricsand assemble fresh records on each call. Take the exact method signature from the API jar, because it may take a device argument or wrap the return type. - If you added the custom
View Detailsmenu action, remove it. Confirm that the gateway's own Details entry renders your metrics. KeepgetMenuActionsonly for pages you actually build.
import com.codahale.metrics.Timer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
final class GroupMetrics {
final int samplingInterval; // as your scheduler runs this group
final Timer timer = new Timer();
volatile int requestCount; // maintained by your scheduler
volatile double queueDuration; // maintained by your scheduler
volatile double overloadFactor; // maintained by your scheduler
GroupMetrics(int samplingInterval) { this.samplingInterval = samplingInterval; }
}
final class DriverMetrics {
final Timer aggregateTimer = new Timer();
final List<GroupMetrics> groups = new ArrayList<>();
volatile int requestCount;
volatile int monitoredItemCount;
void execute(GroupMetrics g, Runnable request) {
long start = System.nanoTime();
try {
request.run();
} finally {
long elapsed = System.nanoTime() - start;
g.timer.update(elapsed, TimeUnit.NANOSECONDS);
aggregateTimer.update(elapsed, TimeUnit.NANOSECONDS);
}
}
SamplingMetrics snapshot() {
List<SamplingMetrics.SamplingGroupStats> stats = new ArrayList<>();
for (GroupMetrics g : groups) {
stats.add(new SamplingMetrics.SamplingGroupStats(
g.requestCount, g.samplingInterval,
g.queueDuration, g.overloadFactor,
g.timer.getSnapshot(),
g.timer.getMeanRate(),
g.timer.getOneMinuteRate()));
}
return new SamplingMetrics(
new SamplingMetrics.AggregateStats(
requestCount, monitoredItemCount,
aggregateTimer.getSnapshot(),
aggregateTimer.getMeanRate(),
aggregateTimer.getOneMinuteRate()),
stats);
}
}
Record into the Timer in a finally block. Failed and timed-out requests then still show up in the histogram, and a device that is timing out is exactly the case you want to see on this page.
Map Timer output onto each SamplingMetrics field
-
histogramcomes fromtimer.getSnapshot(). Pass the Snapshot through unchanged; the gateway serializes it. Codahale Timers store durations internally in nanoseconds, so do not pre-scale the values. -
meanThroughputcomes fromtimer.getMeanRate(), andoneMinuteThroughputfromtimer.getOneMinuteRate(). Both are Codahale rates in events per second. -
samplingInterval(int) is the group's interval as your scheduler runs it. There is no separate field for a revised or "actual" interval. -
requestCount(int) appears at both levels. The name can mean requests per group cycle or cumulative executions. Open the Details page of a built-in driver on the same gateway and match its behavior, so your device reads the same way to operators. -
monitoredItemCount(aggregate only) is the number of monitored items the device currently serves. -
queueDurationandoverloadFactor(double) are not Timer outputs. Compute them in your scheduler. Cross-check units and scale against a built-in driver's Details page before you release.
Use the constructor parameters as the contract, not the serializer's JSON keys. The keys mirror the record components, but the records are the interface you compile against.
Tag driver logs with the device-name MDC key
Log Activity filters on MDC key device-name equal to the device instance name. Build the driver logger with that context attached.
Java:
LoggerEx log = LoggerEx.newBuilder()
.mdcContext(new Object[] { "device-name", context.name })
.build(String.format("drivers.%s", getClass().getSimpleName()));
Kotlin:
val logger = LoggerEx.newBuilder()
.mdcContext("device-name", context.name)
.build("drivers.${javaClass.simpleName}")
- Use the device instance name from the device context. A hard-coded or driver-type name will not match the filter.
- Every class that logs on behalf of a device instance needs a logger built this way. A helper class holding a plain static logger will not appear in Log Activity.
- Third-party protocol libraries log through their own loggers without your MDC context. Their output stays in the main gateway log, not the device panel.
- You cannot set a minimum level or pick loggers for the panel. Control verbosity through normal gateway logger level configuration.
Confirm the Details page populates on a live gateway
- Install the module and create a device instance. Subscribe tags at two or more different scan rates so you get more than one sampling group.
- Go to Connections → Devices → Connections and open Details for the device.
- Check that Aggregate Statistics shows a nonzero request count and monitored item count, and that the histogram moves as requests execute.
- Check that each sampling group appears with the interval you configured. One-minute throughput should settle near the expected request rate after about a minute of steady polling.
- Disconnect the device or block its port. Confirm that the Timers keep recording, so failures show in the histogram, and that the scheduler updates the overload figures.
- Trigger a connect, disconnect, or read error. Confirm the resulting log lines appear under Log Activity for that device only, and not under a second device instance of the same driver.
Work around the API gaps before release
-
No published Javadoc for
opc-ua-gateway-apiat the pre-8.3.0 stage. Your references are the record definitions andDeviceExtensionPointin the jar. Recheck them against the released SDK when you rebuild for 8.3.0 or later. -
No field for the actual per-group sampling interval.
SamplingGroupStats.samplingIntervalis the only interval field. If your driver revises intervals (for example, clamping to a device minimum), decide which value to report and document that for your users. Extending the interface for this is a feature request, not a configuration option. - No control over Log Activity scope. The panel is MDC-filtered only. If your users need more targeted logs, route that detail through dedicated logger names they can adjust in the gateway.
- Timer-centric model. Drivers that don't execute discrete requests, such as push or unsolicited-message protocols, fit the model poorly. Pick a unit of work you can time (for example, per inbound message batch) and report it consistently.
FAQ
How do I get a Details diagnostics page for my custom Ignition 8.3 device driver?
Implement DeviceExtensionPoint::getSamplingMetrics and return a SamplingMetrics record built from AggregateStats and a list of SamplingGroupStats. Adding a MenuAction that points at the built-in DeviceDetails component does not supply any data.
How do I fill the histogram field in SamplingMetrics?
Time each request execution with a com.codahale.metrics.Timer and pass timer.getSnapshot() as the histogram. Use getMeanRate() and getOneMinuteRate() from the same Timer for meanThroughput and oneMinuteThroughput.
How do I make my driver's logs appear in the device Log Activity panel?
Build your logger with LoggerEx.newBuilder().mdcContext("device-name", context.name).build(...). The panel shows only statements whose MDC key device-name matches the device instance name.
Can I set the minimum log level or choose loggers for Log Activity?
No. Log Activity is a fixed filter on the device-name MDC key, and nothing about it is configurable from the driver. Adjust verbosity through the normal gateway logger levels instead.
When should I escalate to Inductive Automation support?
Escalate if getSamplingMetrics returns populated records but the Details page stays empty on a released 8.3 gateway. Also escalate if the method signature or record shapes in the released SDK differ from the ones described here. Include your module's SDK version, the gateway version, and the gateway log excerpt from opening the Details page when you contact official support.