Query traces in Promscale

This section covers information about the data model used with traces and the different SQL queries you can use for trace data.

You can query the data in Promscale with your preferred SQL tool. For example, you can use psql. For more information about installing and using psql, see the installing psql section.

Span

A span represents a single operation within a trace. The structure of a span is similar to:

NameTypeDescription
trace_idtrace_idTrace identifier
span_idint8Span identifier
trace_statetextTrace state
parent_span_idint8Reference to the parent trace_id
is_root_spanboolIs the span a root span
service_nametextName of the service
span_nametextName of the span
span_kindenumSpan kind
start_timetimestamptzStart date and time of the span
end_timetimestamptzEnd date and time of the span
time_rangetstzrangeA tstzrange representation of Start and End times of the span
duration_msfloat8Duration of the span in milliseconds
span_tagstag_mapKey-value pairs for span tags. See details on tag_map type in this section
dropped_tags_countint4Number of dropped tags
event_timetstzrangeStart and end time of the event
dropped_events_countint4Number of dropped events
dropped_link_countint4Number of dropped links
status_codeenumStatus Code
status_messagetextStatus message
instrumentation_lib_nametextInstrumentation Library name
instrumentation_lib_versiontextInstrumentation Library version
instrumentation_lib_schema_urltextInstrumentation Library schema URL
resource_tagstag_mapResource tags
resource_dropped_tags_countint4Number of dropped resources
resource_schema_urltextResource’s schema file URL

tag_map

The tag_map type is a storage optimization for spans. It can be queried as a regular jsonb PostgreSQL type. It normalizes the span data and can significantly reduce the storage footprint. This is a custom type created by Timescale, and it is continuously being improved. While all other operators also perform well, these operators have additional performance optimizations:

  • -> is used get the value for the given key. For example: span_tags -> 'pwlen'.
  • = is used to provide value equality for a key. For example: span_tags -> 'pw_len' = '10'::jsonb.
  • != is used to provide value inequality for a key. For example: span_tags -> 'pw_len' != '10'::jsonb.

trace_id

The trace_id type is a 16-byte type that is a bit like a UUID. It represents the trace_id in compliance with Open Telemetry requirements.

span_kind

The span_kind provides useful performance context in a trace. This information is useful in performance analysis. The possible values for this type are: unspecified, internal, server, client, producer, and consumer.

status_code

The status_code is a special, standardized property for a span. The possible values are: unset, ok, and error.

Views

Promscale uses views to provide a stable interface. The set of views is provided in the ps_trace schema. These include:

  • span
  • link
  • event

Span view

The span view joins several tables so that you can see an overview of the data relevant for a single span. The span is stored across multiple tables, and data is split across several columns for better index support. The table that contains the span data is a hypertable, so it has support for retention and compression.

This is an example of a simple query to view spans for a service in the last 10 minutes on a Linux server:

  1. select
  2. service_name,
  3. duration_ms
  4. from span
  5. where
  6. start_time >= now() - interval '10 minutes'
  7. and span_tags -> 'os_name' = '"linux"'
  8. and span_kind = 'server'
  9. limit 50

Event view

The event view provides access to events and their corresponding spans. For more information about OpenTelemetry events, see the OpenTelemetry documentation for add events and span events.

The structure of an event view is similar to:

NameTypeDescription
trace_idtrace_idTrace identifier
span_idint8Span identifier
timetimestamptzDate and time when the event has occurred
event_nametextName of the event
event_tags tag_mapKey-value pairs for event tags
dropped_tags_countintegerNumber of dropped event tags
trace_statetextTrace State
service_nametextName of the service
span_nametextName of the span
span_kindenumSpan Kind
span_start_timetimestamptzStart date and time of the span
span_end_timetimestamptzEnd date and time of the span
span_time_rangetstzrangeA tstzrange representation of start and end times of the span
span_duration_msfloat8Duration of the span in milliseconds
span_tagstag_mapKey-value pairs for span tags. See details on tag_map type below
dropped_span_tags_countintegerNumber of dropped span tags
resource_tagstag_mapResource tags
resource_dropped_tags_countintegerNumber of dropped resource tags
status_codeenumStatus code
status_messagetextStatus message

This is an example of a simple query to view the events in the last 10 minutes with error:

  1. select
  2. service_name,
  3. span_name,
  4. status_message
  5. from events
  6. where
  7. start_time >= now() - interval '10 minutes'
  8. and event_tags -> 'level' = '"error"'
  9. limit 50

The link view allows you to see spans that have originated from the same trace. In essence, it is a representation of two related spans, with some extra information about the relationship between them. For more information about linked tags, see the OpenTelemtry specification.

The link view adds all the columns in the previous table, as well as these additional columns:

NameTypeDescription
link_tagstag_mapLink tags
dropped_link_tags_countintegerNumber of dropped link tags

Example trace queries in SQL

A trace is a collection of transactions or spans that represents a unique user or API transaction handled by an application and its services.

When you build time series graphs in Grafana, you can use the Grafana $__interval variable.

If you want to configure bucketing, you can use the TimescaleDB time_bucket function with the corresponding Grafana $bucket_interval variable.

You can limit the start_time using the Grafana $__timeFilter variable.

Bigger windows come at a cost, so you should avoid completely removing the start_time filters. If you remove them, searches need to occur across all spans in the database, which could significantly impact your performance.

PostgreSQL has a very versatile interval type, which can be very useful when you create these kinds of queries.

Find the top twenty slowest traces:

  1. select
  2. service_name as "Service",
  3. duration_ms as "Duration (ms)"
  4. from span s
  5. where
  6. $__timeFilter(start_time)
  7. and (
  8. parent_span_id is null
  9. or span_kind = 'server')
  10. order by 2 desc
  11. limit 20

Find the specified request rate per service:

  1. select
  2. time_bucket('$__interval', start_time) as "time",
  3. coalesce(count(*)/date_part('epoch', '$__interval'::interval), 0) as "Request rate"
  4. from ps_trace.span s
  5. where
  6. $__timeFilter(start_time)
  7. and ( span_kind = 'server'
  8. or parent_span_id is null)
  9. and service_name = '${service}'
  10. group by 1
  11. order by 1

Find the average duration per service:

  1. select
  2. service_name as "Service",
  3. avg(duration_ms) as "Average Duration (ms)"
  4. from ps_trace.span s
  5. where
  6. $__timeFilter(start_time)
  7. and (
  8. parent_span_id is null
  9. or span_kind = 'server')
  10. group by 1
  11. order by 1 asc

Find the most common span errors:

  1. select
  2. status_message as "Error",
  3. service_name as "Service",
  4. count(*) as "Occurrences"
  5. from ps_trace.span
  6. where
  7. $__timeFilter(start_time)
  8. and status_code = 'error'
  9. group by 1, 2
  10. order by 3
  11. limit 50

Find the current error ratio:

  1. select
  2. time_bucket('$__interval', start_time) as "Time",
  3. coalesce((count(*) filter (where status_code = 'error'))::numeric / count(*), 0::numeric) as "Error ratio"
  4. from ps_trace.span s
  5. where
  6. $__timeFilter(start_time)
  7. and ( span_kind = 'server'
  8. or parent_span_id is null)
  9. and service_name = '${service}'
  10. group by 1
  11. order by 1

Query resource and span tags

This section contains some example SQL queries that you can use for insight into complex systems and interactions.

Simple queries

The simplest queries you can perform on spans involve only a single table or view. For example, this query returns certain columns of the span view, up to 50 rows:

  1. select
  2. trace_id,
  3. span_id,
  4. span_tags,
  5. resource_tags,
  6. status_message
  7. from span
  8. limit 50

This query returns all columns of the link view, up to 50 rows:

  1. select *
  2. from link
  3. limit 5

These simpler queries can be a good way to start learning a new system.

Filters

In most cases, the volume and diversity of trace data is very high, so you might find that you need to limit the scope of your queries. You can do this with filtering.

note

When you are working with time series data, make sure you define the time window that you are interested in. Because data is partitioned based on the time, omitting the time filters can drastically hinder the performance of the queries.

To filter the data you’re interested in, you can use the SQL where clause. This example queries spans that happened within last 30 minutes. This type of query is far more efficient than those without any filters in the where clause, as it allows the optimizer to eliminate unnecessary chunks from the hypertable. It also uses indexes to locate rows that satisfy the query. For example:

  1. select
  2. trace_id,
  3. span_id,
  4. status_message
  5. from span
  6. where start_time >= now() - interval '30 minutes'
  7. limit 50

For more information about partitioning hypertables, see the partitioning hypertables section.

When you are querying traces, it is important to pinpoint a particular trace tag. You can do this with standard PostgreSQL json operators. For example:

  1. select *
  2. from span
  3. where
  4. start_time >= now() - interval '30 minutes'
  5. and span_tags -> 'pwlen' = '25'
  6. limit 50

In this example, the scope is limited to spans within last 30 minutes, with a pwlen tag, and an exact value of 25. You can specify as many of these as you want, and combine them with logical operations. For example:

  1. select *
  2. from span
  3. where
  4. start_time >= now() - interval '30 minutes'
  5. and span_tags -> 'pwlen' = '25'
  6. and resource_tags -> 'telemetry.sdk.name' = '"opentelemetry"'
  7. limit 50
note

In the previous example, the '"opentelemtry"' string is both single- and double-quoted. This is because PostgreSQL’s -> operator returns JSONB, and expects JSONB on the right side of the =. You could potentially work around this by using the ->> operator instead, so that it returns text instead of JSONB, but the ->> operator is not supported in the current implementation of tag_map type.

Joins

You can use a JOIN to see how the error rate of your service correlates with the overall memory consumption of a container. To do this, you need to join the corresponding metric table, with the span view. For example:

  1. select
  2. time_bucket('30 minutes', span.start_time) as "time",
  3. max(mem.value) as "max_mem",
  4. count(trace_id) as "num_errors"
  5. from container_memory_working_set_bytes as mem
  6. left join span on
  7. time_bucket('30 minutes', span.start_time) = time_bucket('30 minutes', mem.time)
  8. and span.status_code = 'error'
  9. and span.service_name = 'my_service'
  10. and span.span_tags -> 'container_name' = '"my_container"'
  11. where
  12. mem.value != 'NaN'
  13. and mem.time >= now() - interval '1 week'
  14. and span.start_time >= now() - interval '1 week'
  15. and mem.container = 'my_container'
  16. and mem.instance = 42
  17. group by 1
  18. order by 1

To understand the previous example in more depth, start by looking at the metric table and its filters:

  • To retrieve actual memory usage values, the query filters out some misses where the value was reported as NaN.
  • The query specifies last week as the time window. This allows the planner to eliminate unnecessary partitions on the planning stage. This should also significantly speed up the query.
  • The query limits the metrics to the container and instance you’re interested in. This also helps to give the planner more freedom in dealing with the query.
  • The join clause itself is matching only on the generated time_bucket.

Now to look at the span view:

  • The only qualification in the where clause is the start_time matching that of the metric.
  • A number of filters are specified in the join condition instead. This is for a couple of reasons. Firstly, you need to filter out irrelevant rows from the span view. Secondly, you want to keep the entries from the metric table to have a good measure of memory consumption regardless if there were errors or not. If you put these qualifications into the where clause instead, all the rows without errors that match would be filtered out.

Grouping

You can use aggregate functions to perform various operations on groups. You can group data on any set of columns, including fields of tag_map columns.

This example groups by both time and the pwlen field of the span_tags column using their position in the select list. In this case, count(*) is an aggregate function that counts the number of rows in the group. In this case, it’s a 30 minute window with the same value of pwlen field. For example:

  1. select
  2. time_bucket('30 minutes', start_time) as "time",
  3. span_tags -> 'pwlen' as "pwlen",
  4. count(*) as "cnt"
  5. from span
  6. where
  7. start_time >= now() - interval '1 day'
  8. and service_name = '${service}'
  9. group by 1, 2

Sorting

You can also sort data based on tag_map column fields. This behaves in the same way as the standard PostgreSQL jsonb type, and the same rules apply when sorting. Numeric fields sort using numeric comparison, like [1, 2, 10, 11], and text fields sort using string comparison, like ["1", "10", "11", "2"].

In this example, the output is sorted based on the value and type of pwlen:

  1. select *
  2. from span
  3. where start_time >= now() - interval '1 hour'
  4. order by span_tags -> 'pwlen'
  5. limit 50