Logs Schema and Writing ClickHouse Queries for Building Dashboard Panels.
At SigNoz we store our data on ClickHouse. In this documentation, we will go through the schema of the logs table and see how we can write clickhouse queries to create different dashboard panels from Logs Data.
Logs Schema
If we check the schema of the logs table in clickhouse this is what it looks like. The table was created with respect to the OpenTelemetry Logs Data Model
CREATE TABLE signoz_logs.logs
(
`timestamp` UInt64 CODEC(DoubleDelta, LZ4),
`observed_timestamp` UInt64 CODEC(DoubleDelta, LZ4),
`id` String CODEC(ZSTD(1)),
`trace_id` String CODEC(ZSTD(1)),
`span_id` String CODEC(ZSTD(1)),
`trace_flags` UInt32,
`severity_text` LowCardinality(String) CODEC(ZSTD(1)),
`severity_number` UInt8,
`body` String CODEC(ZSTD(2)),
`resources_string_key` Array(String) CODEC(ZSTD(1)),
`resources_string_value` Array(String) CODEC(ZSTD(1)),
`attributes_string_key` Array(String) CODEC(ZSTD(1)),
`attributes_string_value` Array(String) CODEC(ZSTD(1)),
`attributes_int64_key` Array(String) CODEC(ZSTD(1)),
`attributes_int64_value` Array(Int64) CODEC(ZSTD(1)),
`attributes_float64_key` Array(String) CODEC(ZSTD(1)),
`attributes_float64_value` Array(Float64) CODEC(ZSTD(1)),
`attributes_bool_key` Array(String) CODEC(ZSTD(1)),
`attributes_bool_value` Array(Bool) CODEC(ZSTD(1)),
INDEX body_idx body TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
INDEX id_minmax id TYPE minmax GRANULARITY 1,
INDEX severity_number_idx severity_number TYPE set(25) GRANULARITY 4,
INDEX severity_text_idx severity_text TYPE set(25) GRANULARITY 4,
INDEX trace_flags_idx trace_flags TYPE bloom_filter GRANULARITY 4
)
ENGINE = MergeTree
PARTITION BY toDate(timestamp / 1000000000)
ORDER BY (timestamp, id)
TTL toDateTime(timestamp / 1000000000) + toIntervalSecond(1296000)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
There is a distributed logs table which references the above table in each shard. The name of the table is distributed_logs
. The schema is same as above.
Note:- Any query that we write will be written for the distributed_logs
table.
Columns in the Logs Table
NAME | DESCRIPTION |
---|---|
timestamp | Time when the log line was generated at the source. The default value is the time at which it is received and it can be changed using the time parser. |
observed_timestamp | Time when the log line is observed at the collection system. It is automatically added by the collector. |
id | It is a ksuid, it helps us in paginating and sorting log lines. It is automatically added by the collector. |
trace_id | Trace ID of the log line. W3C Trace Context. It can be filled using trace parser. |
span_id | Span ID for the log line or set of log lines that are part of a particular processing span. It can be filled using trace parser. |
trace_flags | Trace Flag of the log line. W3C Trace Context. It can be filled using trace parser. |
severity_text | It is the log level. eg:- info . It can be filled using severity parser. |
severity_number | Numerical value of the severity_text. It can be filled using severity parser. |
body | The body/message of the log record. |
resources_string_key | If we have a resource named source: nginx . Then source is stored in this column as an array value. |
resource_string_value | If we have a resource named source: nginx . Then nginx is stored in this column as an array value and the index will be same as the corresponding key in resources_string_key . |
attributes_string_key | If we have a string attribute named method: GET . Then method is stored in this column as an array value. |
attributes_string_value | If we have a string attribute named method: GET . Then GET is stored in this column as an array value and the index will be same as the corresponding key in attributes_string_key . |
attributes_int64_key | If we have a integer attribute named bytes: 100 . Then bytes is stored in this column as an array value. |
attributes_int64_value | If we have a integer attribute named bytes: 100 . Then 100 is stored in this column as an array value and the index will be same as the corresponding key in attributes_int64_key . |
attributes_float64_key | If we have a floating attribute named delay: 10.0 . Then delay is stored in this column as an array value. |
attributes_float64_value | If we have a floating attribute named dealy: 10.0 . Then 10.0 is stored in this column as an array value and the index will be same as the corresponding key in attributes_float64_key . |
attributes_bool_key | If we have a boolean attribute named success: true . Then success is stored in this column as an array value. |
attributes_bool_value | If we have a boolean attribute named success: true . Then true is stored in this column as an array value and the index will be same as the corresponding key in attributes_bool_key . |
The attributes and resources can be added and transformed using different processors and operators. You can read more about them here
Selected Attributes/Resources:-
When an attribute/resource field is converted to selected(indexed) field. Then two new columns are added.
Ex: if our attribute name is method
which is present in attributes_string_key
and it's value is present in attributes_string_value
then the corresponding columns that will be created are attribute_string_method
and attribute_string_method_exists
. It will look like following in the logs schema.
`attribute_string_method` String MATERIALIZED attributes_string_value[indexOf(attributes_string_key, 'xyz')] CODEC(ZSTD(1)),
`attribute_string_method_exists` Bool MATERIALIZED if(indexOf(attributes_string_key, 'xyz') != 0, true, false) CODEC(ZSTD(1)),
Writing Clickhouse Queries for Dashboard Panels
While writing queries for logs table, if you want to use an attribute/resource attribute in your query you will have to reference it in the following format
<type>_<dataType>_value[indexOf(<type>_<dataType>_key, <keyname>)]
where type
can be attributes/resources
, dataType
can be int64/float64/string
and keyname
is the name of the key.
Eg: If your keyname
is status
of dataType
string
and type
attribute
, it needs to be referenced as attributes_string_value[indexOf(attributes_string_key, 'status')]
Note:- In the above example, if status
is an selected field , then it can be referenced as
attribute_string_status
We will use two variables i.e {{.start_timestamp_nano}}
and {{.end_timestamp_nano}}
while writing our queries to specify the time range.
Timeseries
This panel is used when you want to view your aggregated data in a timeseries.
Examples
Count of log lines per minute
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}})
GROUP BY ts
ORDER BY ts ASC;
Count of log lines per minute group by container name
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
attributes_string_value[indexOf(attributes_string_key, 'container_name')] as container_name,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
indexOf(attributes_string_key, 'container_name') > 0
GROUP BY container_name, ts
ORDER BY ts ASC;
Count of log lines per minute where severity_text = 'INFO'
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
severity_text='INFO'
GROUP BY ts
ORDER BY ts ASC;
Count of log lines per minute where severity_text = 'INFO'
, method = 'GET'
, service_name = 'demo'
. Here method
is an attribute while service_name
is a resource attribute.
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
severity_text='INFO' AND
attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND
resources_string_value[indexOf(resources_string_key, 'service_name')] = 'demo'
GROUP BY ts
ORDER BY ts ASC;
Count of log lines per minute where severity_text = 'INFO'
, method = 'GET'
, service_name = 'demo'
. Here method
is an attribute while service_name
is a resource attribute and both method
and service_name
is selected field.
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
severity_text='INFO' AND
attribute_string_method = 'GET' AND
resource_string_service_name = 'demo'
GROUP BY ts
ORDER BY ts ASC;
Value
For the value type panel, the overall query will be similar to timeseries, just that you will have to get the absolute value at the end. You can reduce your end result to either average, latest, sum, min, or max.
Examples
Average count of log lines where severity_text = 'INFO'
, method = 'GET'
, service_name = 'demo'
. Here method
is an attribute while service_name
is a resource attribute.
SELECT
avg(value) as value,
any(ts) as ts FROM (
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
severity_text='INFO' AND
attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' AND
resources_string_value[indexOf(resources_string_key, 'service_name')] = 'demo'
GROUP BY ts
ORDER BY ts ASC
)
Note :- attributes_string_value[indexOf(attributes_string_key, 'method')]
will change to attribute_string_method
if method
is a selected field and resources_string_value[indexOf(resources_string_key, 'service_name')]
will change to resource_string_service_name
if service_name
is a selected field.
Table
This is used when you want to view the timeseries data in a tabular format.
The query is similar to timeseries query but instead of using time interval we use just use now() as ts
in select.
Examples
Count of log lines where severity_text = 'INFO'
, method = 'GET'
group by service_name
. Here method
is an attribute while service_name
is a resource attribute.
SELECT
now() as ts,
resources_string_value[indexOf(resources_string_key, 'service_name')] as service_name,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
severity_text='INFO' AND
attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET'
GROUP BY service_name, ts
ORDER BY ts ASC;
Note :- attributes_string_value[indexOf(attributes_string_key, 'method')]
will change to attribute_string_method
if method
is a selected field and resources_string_value[indexOf(resources_string_key, 'service_name')]
will change to resource_string_service_name
if service_name
is a selected field.
Real Life Use Cases Example
Number of log lines generated by each kubernetes cluster
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
resources_string_value[indexOf(resources_string_key, 'k8s_cluster_name')] as k8s_cluster_name,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
indexOf(resources_string_key, 'k8s_cluster_name') > 0
GROUP BY k8s_cluster_name, ts
ORDER BY ts ASC;
Note:- resources_string_value[indexOf(resources_string_key, 'k8s_cluster_name')]
will change to resource_string_k8s_cluster_name
if k8s_cluster_name
is a selected field and
indexOf(resources_string_key, 'k8s_cluster_name') > 0
will change to resource_string_k8s_cluster_name_exists = true
Number of error logs generated by each service
SELECT
toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
resources_string_value[indexOf(resources_string_key, 'service_name')] as service_name,
toFloat64(count()) AS value
FROM
signoz_logs.distributed_logs
WHERE
(timestamp >= {{.start_timestamp_nano}} AND timestamp <= {{.end_timestamp_nano}}) AND
severity_text='ERROR' AND
indexOf(resources_string_key, 'service_name') > 0
GROUP BY service_name,ts
ORDER BY ts ASC;
Note:- resources_string_value[indexOf(resources_string_key, 'service_name')]
will change to resource_string_service_name
if service_name
is a selected field and
indexOf(resources_string_key, 'service_name') > 0
will change to resource_string_service_name_exists = true
Panel Time preference
Using the Panel Time Preference
present on the right you can select a custom time range for your panel. When you open the dashboard the specific panel will render for the time
specified for that panel.