Configuring Ignition 8.3 Device Details Pages in Custom Drivers

Mark Townsend9 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

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:
    public List<MenuAction> getMenuActions() {
        return List.of(new MenuAction("View Details", "View device details.",
            (WebUiComponent) new ReactComponentInfo("DeviceDetails", null)));
    }
    The menu entry appears, and the React component loads with null props. Nothing on the gateway side hands it any statistics. getMenuActions is 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 SamplingMetricsSerializer that writes an aggregate object and a samplingGroups array. You do not produce that JSON yourself. You return typed SamplingMetrics records, 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-name equals 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-api module 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 SamplingMetrics object returned by DeviceExtensionPoint::getSamplingMetrics. The gateway serializes it into an aggregate block and a samplingGroups array.
  • 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

  1. Create one Timer for 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.
  2. Wrap every request execution. Record the elapsed time into both the group Timer and the aggregate Timer.
  3. Track the non-Timer fields in your scheduler: request count per group, monitored item count, queue duration, and overload factor.
  4. Override getSamplingMetrics and 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.
  5. If you added the custom View Details menu action, remove it. Confirm that the gateway's own Details entry renders your metrics. Keep getMenuActions only 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

  • histogram comes from timer.getSnapshot(). Pass the Snapshot through unchanged; the gateway serializes it. Codahale Timers store durations internally in nanoseconds, so do not pre-scale the values.
  • meanThroughput comes from timer.getMeanRate(), and oneMinuteThroughput from timer.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.
  • queueDuration and overloadFactor (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

  1. 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.
  2. Go to Connections → Devices → Connections and open Details for the device.
  3. Check that Aggregate Statistics shows a nonzero request count and monitored item count, and that the histogram moves as requests execute.
  4. 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.
  5. 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.
  6. 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-api at the pre-8.3.0 stage. Your references are the record definitions and DeviceExtensionPoint in 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.samplingInterval is 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.

Back to blog