Product - graph.

Custom Metrics & Dashboards for Embedded Devices

April 28, 2026 • By Michael Mikuš

Spotflow automatically collects system metrics from your devices: CPU utilization, heap usage, stack pressure, MQTT connection state, reset causes. These built-in metrics are valuable for detecting memory leaks before they cause crashes, or spotting a CPU spike introduced by a firmware update.

But for most connected products, system health is only part of what you need to understand.

You also care about what your device is actually doing in the field. How many times was the door unlocked today? Which unlock method are users reaching for most? How long does an NFC authentication take, and does that change between firmware versions? Is battery drain correlated with a specific user workflow?

This post walks through Spotflow’s Custom Metrics and Custom Dashboards features using a smart lock fleet as a running example. By the end, you will know how to define and report application-specific metrics from Zephyr or any MQTT-capable device, and how to build dashboards that go beyond infrastructure monitoring into product analytics.

Key Takeaways

  • Custom Metrics let you track application-specific data (sensor readings, operation durations, button presses, business events) alongside built-in system telemetry.
  • Zephyr integration is a few lines of C: register a metric handle at startup, report values from anywhere in your application. The SDK handles aggregation, encoding, and MQTT transmission.
  • MQTT integration works for any platform (FreeRTOS, bare metal, ESP-IDF, Linux): publish a payload to a single topic.
  • Labels add dimensions to a metric: each unique combination of label values creates a separate time series with independent aggregation. You declare the label keys once when registering the metric and supply their values with each report.
  • Custom Dashboards give you product-specific views with tailored queries, filters, and groupings. The same pipeline that powers operational monitoring also powers product analytics.

The Limits of System Metrics Alone

Spotflow collects a set of system metrics automatically when you enable CONFIG_SPOTFLOW_METRICS_SYSTEM=y on Zephyr. These include:

These metrics are surfaced automatically in the built-in Device Dashboard and Overview Dashboard. They answer the question: Is the device healthy?

What they do not answer is: What is the device doing, and how are users interacting with it?

For a smart lock, you might deploy a firmware version that looks perfectly healthy by all system metrics (no crashes, stable CPU, heap well within limits) but still have users reporting that NFC unlocks feel slow, or that Bluetooth pairing started failing on a subset of devices after the latest OTA. These issues are invisible to system telemetry. They require application-level instrumentation.

A Concrete Example: Smart Lock Fleet

Imagine you operate a fleet of smart locks deployed across a portfolio of commercial buildings. Each lock runs firmware on a low-power ARM Cortex-M33 MCU with Bluetooth, NFC, and keypad input, connected to the cloud via MQTT over TLS.

The device has several key behaviors you want to observe from the cloud:

None of these are captured by system metrics. They require custom metrics defined in your application. The smart_lock_fleet sample on GitHub is the complete runnable project used as the basis for the code in this post.

Reporting Custom Metrics from Zephyr

If your device runs Zephyr RTOS (or Nordic nRF Connect SDK), the Spotflow module handles aggregation, encoding, and MQTT transmission automatically. You register metric handles at startup and report raw values from your application. The SDK does the rest.

Enable Metrics in Kconfig

Make sure the Spotflow module and metrics subsystem are enabled in your prj.conf:

CONFIG_SPOTFLOW=y

# Enable custom (and optionally system) metrics
CONFIG_SPOTFLOW_METRICS=y
# Optional: auto-collect CPU, heap, stack, etc.
CONFIG_SPOTFLOW_METRICS_SYSTEM=y

CONFIG_SPOTFLOW_METRICS_SYSTEM is independent of custom metrics. You can enable both together, or skip system metrics and use only custom metrics. Your choice.

Before integrating your device with Spotflow, make sure you have established a connection to the internet from the device.

Include the Metrics Header

In your application source file, include the Spotflow metrics API:

#include "metrics/spotflow_metrics_backend.h"

Register Your Custom Metrics

Register each metric before reporting any values. Registration binds a metric name and aggregation interval to a typed handle used for all subsequent reports.

In the smart lock sample, the three lock metrics are registered together in init_lock_metrics() in lock.c, called once from main() after the network is up:

/* Handles - module-level statics in lock.c */
static struct spotflow_metric_float *g_op_duration_metric;
static struct spotflow_metric_int   *g_door_opened_metric;
static struct spotflow_metric_int   *g_auth_failure_metric;

int init_lock_metrics(void)
{
    int rc;

    /*
     * lock_operation_duration_ms: float, 1-minute aggregation.
     * max_timeseries = 6: 2 operations (lock/unlock) x 3 methods = 6 label combinations.
     * max_labels = 2: "operation" and "method".
     */
    rc = spotflow_register_metric_float_with_labels(
        "lock_operation_duration_ms",
        SPOTFLOW_AGG_INTERVAL_1MIN,
        6,  /* max_timeseries */
        2,  /* max_labels */
        &g_op_duration_metric);
    if (rc < 0) {
        LOG_ERR("Failed to register lock_operation_duration_ms: %d", rc);
        return rc;
    }

    /*
     * door_opened: int, no aggregation.
     * Every door open event is transmitted immediately as a discrete data point.
     */
    rc = spotflow_register_metric_int(
        "door_opened",
        SPOTFLOW_AGG_INTERVAL_NONE,
        &g_door_opened_metric);
    if (rc < 0) {
        LOG_ERR("Failed to register door_opened: %d", rc);
        return rc;
    }

    /*
     * auth_failure: int, no aggregation, labeled by method.
     * max_timeseries = 3: one series per method (nfc, keypad, bluetooth).
     * max_labels = 1: "method".
     */
    rc = spotflow_register_metric_int_with_labels(
        "auth_failure",
        SPOTFLOW_AGG_INTERVAL_NONE,
        3,  /* max_timeseries */
        1,  /* max_labels */
        &g_auth_failure_metric);
    if (rc < 0) {
        LOG_ERR("Failed to register auth_failure: %d", rc);
    }

    return rc;
}

The battery metric is registered differently: inside the battery monitor thread itself. Registering the metric in the same source file and thread where it will be used keeps the handle and reporting loop together, with no external dependency. The thread is started automatically at boot via K_THREAD_DEFINE; the metric registration happens when the thread first runs:

static struct spotflow_metric_float *g_battery_metric;

static void battery_thread_entry(void)
{
    int rc = spotflow_register_metric_float(
        "battery_level_percent",
        SPOTFLOW_AGG_INTERVAL_NONE,
        &g_battery_metric);
    if (rc < 0) {
        LOG_ERR("Failed to register battery_level_percent: %d", rc);
        return;
    }

    while (true) {
        float level = read_battery_percent();
        spotflow_report_metric_float(g_battery_metric, level);
        k_sleep(K_MSEC(BATTERY_REPORT_INTERVAL_MS)); /* every 5 minutes */
    }
}

K_THREAD_DEFINE(battery_thread, BATTERY_THREAD_STACK_SIZE,
                battery_thread_entry, NULL, NULL, NULL,
                BATTERY_THREAD_PRIORITY, 0, 0);

K_THREAD_DEFINE creates and starts the thread at system init, before main() runs. Registering the metric inside the thread entry keeps the handle and the reporting loop in the same translation unit, with no external dependency.

Choosing the Right Aggregation Interval

The aggregation interval is a registration-time decision: you choose it once when calling spotflow_register_metric_*, and it applies to every value reported through that handle. It is a trade-off between data granularity and bandwidth usage. Spotflow supports four options:

IntervalConstant (Zephyr)JSON valueTypical use case
No aggregationSPOTFLOW_AGG_INTERVAL_NONE"0"Events, state changes, point-in-time readings where every sample matters
1 minuteSPOTFLOW_AGG_INTERVAL_1MIN"1m"Operation durations, sensor readings where per-minute trends are sufficient
1 hourSPOTFLOW_AGG_INTERVAL_1HOUR"1h"Fleet health trends, long-running deployments, capacity planning
1 daySPOTFLOW_AGG_INTERVAL_1DAY"1d"Low-bandwidth deployments (LoRa, NB-IoT), daily usage summaries

For the smart lock: door open events and authentication failures use no aggregation, so every occurrence is a distinct data point visible individually. Operation duration uses 1-minute aggregation, because you care about average latency trends and outliers, not each individual millisecond measurement. Battery level sampled every 5 minutes is sent raw here, but could be aggregated to 1-hour resolution if bandwidth is a concern.

When using the Spotflow device SDK (e.g. Zephyr), aggregation is computed on-device and the SDK transmits sum, count, min, and max as a single MQTT message at the end of each window.

Report Values from Application Code

Once registered, report values at any point from any thread. In lock.c, three static functions handle reporting and are called from simulate_lock_operation():

static void on_lock_operation_complete(const char *operation,
                                       const char *method,
                                       float duration_ms)
{
    struct spotflow_label labels[] = {
        { .key = "operation", .value = operation }, /* "lock" or "unlock" */
        { .key = "method",    .value = method    }, /* "nfc", "keypad", or "bluetooth" */
    };

    int rc = spotflow_report_metric_float_with_labels(
        g_op_duration_metric, duration_ms, labels, 2);
    if (rc < 0) {
        LOG_ERR("Failed to report lock_operation_duration_ms: %d", rc);
    }
}

Because this metric uses SPOTFLOW_AGG_INTERVAL_1MIN, the SDK accumulates all reported values within each one-minute window and transmits a single aggregated message (sum, count, min, max) at the end of the window. On a device reporting dozens of operations per minute, this reduces outgoing MQTT messages to one per minute per label combination, regardless of operation frequency.

For door opens and authentication failures, use the event API. An event is equivalent to reporting an integer value of 1 with no aggregation — it signals that something happened:

static void on_door_opened(void)
{
    int rc = spotflow_report_event(g_door_opened_metric);
    if (rc < 0) {
        LOG_ERR("Failed to report door_opened: %d", rc);
    }
}

static void on_auth_failure(const char *method)
{
    struct spotflow_label labels[] = {
        { .key = "method", .value = method },
    };

    int rc = spotflow_report_event_with_labels(g_auth_failure_metric, labels, 1);
    if (rc < 0) {
        LOG_ERR("Failed to report auth_failure: %d", rc);
    }
}

Battery level reporting lives entirely inside battery_thread_entry() shown above. The read_battery_percent() call is where your platform ADC read would go; in the sample it returns a simulated slowly-draining value. The rest of the thread is identical to what you would write on real hardware.

That is the complete application-side instrumentation. No Spotflow-specific logic leaks into your business logic paths. Just report a value when something happens, and the SDK takes care of the rest.

For the full Zephyr integration reference, see Metrics with Zephyr. The complete runnable project for this smart lock example is in the smart_lock_fleet sample on GitHub; the canonical minimal reference is the metrics sample.

The diagram below illustrates the full data flow inside the Zephyr SDK:

Zephyr metrics data flow: application calls spotflow_report_metric, SDK aggregates and encodes in CBOR, MQTT thread transmits to Spotflow over TLS. Zephyr metrics data flow: application calls spotflow_report_metric, SDK aggregates and encodes in CBOR, MQTT thread transmits to Spotflow over TLS.

If your device does not run Zephyr, custom metrics can be reported directly over MQTT from any platform (FreeRTOS, bare metal, ESP-IDF, Linux). Connect to mqtt.spotflow.io:8883 over TLS and publish JSON payloads to the ingest-json topic with metricName, sum, and optionally labels, aggregationInterval, count, min, and max. When using the MQTT integration directly, you construct the aggregated payload yourself. If no aggregation is needed, set the aggregation window to 0 and only the sum field is required (it represents the raw sample value). See Metrics with MQTT for the full payload schema.

Building a Custom Dashboard

Once metrics are flowing into Spotflow, you can build a custom dashboard to visualize them alongside or separately from the built-in device and fleet views.

Step 1: Create a New Dashboard

In the Spotflow portal, navigate to Dashboards and click + Create Dashboard. Give it a name, for example Smart Lock Fleet Analytics, and an optional description.

Spotflow Create Dashboard dialog with name field filled in as "Smart Lock Fleet Analytics".
Spotflow Create Dashboard dialog with name field filled in as "Smart Lock Fleet Analytics".

Step 2: Add a Custom Widget

Click Add Your First Widget and choose Create Custom Widget. The widget builder lets you configure:

Spotflow custom widget builder configured for lock_operation_duration_ms, grouped by method label, displayed as a line chart in milliseconds.
Spotflow custom widget builder configured for lock_operation_duration_ms, grouped by method label, displayed as a line chart in milliseconds.

Step 3: Build the Full Dashboard

For the smart lock fleet, a useful analytics dashboard includes:

After saving, configure the time range (last 24 hours, 7 days, 30 days) and auto-refresh frequency. Widgets can be resized and renamed at any time, and you can create multiple dashboards for different audiences: one for the firmware team focused on latency and errors, another for product management focused on usage patterns.

Completed Smart Lock Fleet Analytics custom dashboard in Spotflow showing operation duration by method, door open events, authentication failure rates, and battery levels.
Completed Smart Lock Fleet Analytics custom dashboard in Spotflow showing operation duration by method, door open events, authentication failure rates, and battery levels.

For step-by-step instructions, see Create Custom Dashboard.

Product Analytics: Business Insights from Firmware Data

Custom metrics are not limited to operational monitoring. The same pipeline (device reports a metric, Spotflow ingests and stores it, dashboard visualizes it) works equally well for product analytics.

For the smart lock product, the metrics you are already collecting to debug firmware issues also answer product questions:

The key insight for embedded engineers: product analytics do not require a separate data infrastructure. The metrics your firmware reports for debugging and operational monitoring are the same metrics that can drive product decisions, as long as you instrument for both from the start.

A temperature sensor product becomes: “Which geographic regions are experiencing readings outside the expected range?” An industrial pump controller becomes: “How often is the pressure relief valve activating per installation, and has that changed with the latest control loop tuning?” A medical infusion device becomes: “What is the distribution of alarm acknowledgment times across hospitals?”

These questions are answered by the same custom metrics pipeline. The difference is what you choose to measure and how you label it.

Conclusion

Spotflow custom metrics let you go beyond built-in system telemetry and define the measurements that actually matter to your product. From sensor readings and operation durations to user interaction events, any application-level data point can be captured, aggregated on-device, and transmitted to Spotflow using the Spotflow device SDK (e.g. Zephyr) or via the MQTT integration directly.

Custom dashboards then let you build product-specific views of that data. Operational charts help you debug fleet issues; product analytics charts answer business questions. Both use the same infrastructure and the same metrics pipeline.

The smart lock example in this post illustrates the pattern, but the same approach applies to any connected embedded product: industrial sensors, medical devices, consumer electronics, agricultural monitors, automotive ECUs. If you can express what you want to know as a numeric measurement or a countable event, you can track it with Spotflow custom metrics.


Ready to get started? Sign up for Spotflow, no credit card required.

Explore the documentation to go deeper:

Questions or feedback? Reach out on Discord or email hello@spotflow.io.

Spotflow ©

SOCIALS & CONTACT

>>>>>>>>>>>>>>>>>>>>>>>

COMPARE

>>>>>>>>>>