documentation/cookbook/integrations/opcua-dense-format.md
Configure Telegraf to collect OPC-UA industrial automation data and insert it into QuestDB in a dense format. By default, Telegraf creates one row per metric with sparse columns, but for QuestDB it's more efficient to merge all metrics from the same timestamp into a single dense row.
When using Telegraf's OPC-UA input plugin with the default configuration, each metric value generates a separate row. Even when multiple metrics are collected at the same timestamp, they arrive as individual sparse rows:
Sparse format (inefficient):
| timestamp | ServerLoad | ServerRAM | ServerIO |
|---|---|---|---|
| 2024-01-15T10:00:00.000000Z | 45.2 | NULL | NULL |
| 2024-01-15T10:00:00.000000Z | NULL | 8192.0 | NULL |
| 2024-01-15T10:00:00.000000Z | NULL | NULL | 1250.5 |
This wastes storage space and makes queries more complex.
Dense format (efficient):
| timestamp | ServerLoad | ServerRAM | ServerIO |
|---|---|---|---|
| 2024-01-15T10:00:00.000000Z | 45.2 | 8192.0 | 1250.5 |
Configure Telegraf to merge metrics with matching timestamps and tags before sending to QuestDB. This requires two key changes:
merge aggregator to combine rows[agent]
omit_hostname = true
# OPC-UA Input Plugin
[[inputs.opcua]]
endpoint = "${OPCUA_ENDPOINT}"
connect_timeout = "30s"
request_timeout = "30s"
security_policy = "None"
security_mode = "None"
auth_method = "Anonymous"
name_override = "${METRICS_TABLE_NAME}"
[[inputs.opcua.nodes]]
name = "ServerLoad"
namespace = "2"
identifier_type = "s"
identifier = "Server/Load"
default_tags = { source="opcua_merge" }
[[inputs.opcua.nodes]]
name = "ServerRAM"
namespace = "2"
identifier_type = "s"
identifier = "Server/RAM"
default_tags = { source="opcua_merge" }
[[inputs.opcua.nodes]]
name = "ServerIO"
namespace = "2"
identifier_type = "s"
identifier = "Server/IO"
default_tags = { source="opcua_merge" }
# Merge Aggregator
[[aggregators.merge]]
drop_original = true
tags = ["source"]
# QuestDB Output via ILP
[[outputs.influxdb_v2]]
urls = ["${QUESTDB_HTTP_ENDPOINT}"]
token = "${QUESTDB_HTTP_TOKEN}"
content_encoding = "identity"
1. Common tag
default_tags = { source="opcua_merge" }
Adds the same tag value (source="opcua_merge") to all metrics. The merge aggregator uses this to identify which metrics should be combined.
2. Merge aggregator
[[aggregators.merge]]
drop_original = true
tags = ["source"]
drop_original = true: Discards the original sparse rows after mergingtags = ["source"]: Merges metrics with matching source tag values and the same timestamp3. QuestDB output
[[outputs.influxdb_v2]]
urls = ["${QUESTDB_HTTP_ENDPOINT}"]
content_encoding = "identity"
content_encoding = "identity": Disables gzip compression (QuestDB doesn't require it)The data flow is:
source="opcua_merge" tagsource tagThe merge aggregator combines metrics when:
source) have the same valuesIf metrics have different timestamps or tag values, they won't be merged.
If your OPC-UA nodes have additional tags with different values, those tags will prevent merging. Solutions:
Use the override processor to remove unwanted tags:
[[processors.override]]
[processors.override.tags]
node_id = "" # Removes the 'node_id' tag
namespace = "" # Removes the 'namespace' tag
Use the converter processor to convert tags to fields (fields don't affect merging):
[[processors.converter]]
[processors.converter.tags]
string = ["node_id", "namespace"]
This converts the tags to string fields, which won't interfere with the merge aggregator.
If you don't want the source tag in your final QuestDB table:
# Place this AFTER the merge aggregator
[[processors.override]]
[processors.override.tags]
source = "" # Removes the 'source' tag
Use environment variables for sensitive configuration:
export OPCUA_ENDPOINT="opc.tcp://your-opcua-server:4840"
export METRICS_TABLE_NAME="industrial_metrics"
export QUESTDB_HTTP_ENDPOINT="http://questdb-host:9000"
export QUESTDB_HTTP_TOKEN="your_token_here"
Alternatively, use a .env file:
# .env file
OPCUA_ENDPOINT=opc.tcp://localhost:4840
METRICS_TABLE_NAME=opcua_metrics
QUESTDB_HTTP_ENDPOINT=http://localhost:9000
QUESTDB_HTTP_TOKEN=
Then start Telegraf with:
telegraf --config telegraf.conf
Query QuestDB to verify the data format:
SELECT * FROM opcua_metrics
ORDER BY timestamp DESC
LIMIT 10;
Expected: Dense rows with all metrics populated:
| timestamp | source | ServerLoad | ServerRAM | ServerIO |
|---|---|---|---|---|
| 2024-01-15T10:05:00.000000Z | opcua_merge | 47.8 | 8256.0 | 1305.2 |
| 2024-01-15T10:04:00.000000Z | opcua_merge | 45.2 | 8192.0 | 1250.5 |
Problem: Sparse rows with NULL values:
| timestamp | source | ServerLoad | ServerRAM | ServerIO |
|---|---|---|---|---|
| 2024-01-15T10:05:00.000000Z | opcua_merge | 47.8 | NULL | NULL |
| 2024-01-15T10:05:00.000000Z | opcua_merge | NULL | 8256.0 | NULL |
If you see sparse rows, check:
default_tagsFor higher throughput, use TCP instead of HTTP:
[[outputs.socket_writer]]
address = "tcp://questdb-host:9009"
Differences:
Choose TCP when:
Choose HTTP when:
To collect from multiple OPC-UA servers into separate tables:
# Server 1
[[inputs.opcua]]
endpoint = "opc.tcp://server1:4840"
name_override = "server1_metrics"
[[inputs.opcua.nodes]]
name = "Temperature"
namespace = "2"
identifier_type = "s"
identifier = "Sensor/Temp"
default_tags = { source="server1" }
# Server 2
[[inputs.opcua]]
endpoint = "opc.tcp://server2:4840"
name_override = "server2_metrics"
[[inputs.opcua.nodes]]
name = "Pressure"
namespace = "2"
identifier_type = "s"
identifier = "Sensor/Press"
default_tags = { source="server2" }
# Merge by source tag
[[aggregators.merge]]
drop_original = true
tags = ["source"]
This creates two tables (server1_metrics, server2_metrics) with merged metrics from each server.
:::tip Performance Tuning For high-frequency OPC-UA data:
flush_interval to batch more dataaggregators.merge.period to specify merge window duration:::warning Timestamp Precision OPC-UA timestamps may have different precision than QuestDB expects. Ensure:
:::info Related Documentation