I had been using Prometheus to display Opentelemetry data from MQ on z/OS, but was having problems getting it to display what I wanted. I realised I did not understand how Prometheus stores and uses its data.
Prometheus handles time stamp data very well, “At this time, here are the metrics”. Prometheus provides the capability to display the data in many formats.
I found this course very clear and helpful. I did not read it end to end, but went to the topics of interest.
There is a discussion about metrics from Opentelemetry which you might wish to read after understanding the basics.
There are different sorts of data
Some numbers go up and down
The value of the depth of a queue can go up and down, and you are usually interested in the value at a certain time.
If you have the queue depth at the start of an interval, and at the end of the interval, you cannot take the average of the values. For example if the depth of the queue at midnight is 1000, and at one minute afterwards the queue is emptied, and stays empty till 1 AM, when it gets to depth 1000 – the “average” does not mean anything.
Some numbers just increase
An example of numbers that just increase, is the number of requests processed since the application started. The absolute number is not important, because the longer the application is active, the larger the number. The graph is just a line that goes up. You are more interested in questions like “how many requests happened in the last half hour”. This requires two values at different times.
With the requests, you might have the count of the requests, and also the sum of the duration of the requests. To find the average you calculate the sum/count. This is a true, but unhelpful statement.
If your application has been active for a week, calculating sum/count will give you the overall average. After midnight when there is no transaction activity, the average stays the same, non-zero value. This is a boring metric.
It is more interesting to take a time range, for example 1 minute, and calculate the sum of the durations of records in this hour/count of records in this minute. Then at the end of every minute, display the average value for the preceding minute. Interesting charts include
- The number of requests in the last minute
- The average response time during the last minute.
The key lesson when using these ever increasing values, is you take two time stamps and do calculations on the differences between the the two times.
Instance data and range data
Prometheus has two “types” of data. Instance value where there is one time stamp involved (what is the value at this time?), and Range values where more than one timestamp is involved, for example the increase of a value between two timestamps.
Some functions need a scalar (instance) value, other functions need a range value of two timestamps.
With some functions, I kept using Instance value data – when I should have been using Range values. So I wrote this blog post to help me understand the data model.
Data has attributes
A data item has information
- The label, such as count_of_requests, or sum_of_durations
- Value, this could be a integer, or a string (“OK”)
- A timestamp
- Attributes. Prometheus calls these dimensions, other products call these, tags or meta-data. These should be enough to identify the source, and attributes about the source which might be interesting in analysis or reports.
Example data
Below is some example data
| Label | Timestamp | Value | Attributes. |
|---|---|---|---|
| total_count_requests | … 19:30:04.123456 | 6000 | {jobname=”MYJOB”, request=”database”} |
| total_count_requests | … 20:00:00.987654 | 7000 | {jobname=”MYJOB”, request=”database”} |
| total_duration_requests | … 19:30:04.123456 | 90000 | {jobname=”MYJOB”, request=”database”} |
| total_duration_requests | … 20:00:00.987654 | 95000 | {jobname=”MYJOB”, request=”database”} |
| total_count_requests | … 19:30:04.123456 | 400 | {jobname=”MYJOB”, request=”webserver”} |
| total_duration_requests | … 19:30:04.123456 | 4000 | {jobname=”MYJOB”, request=”webserver”} |
| total_count_requests | … 19:45:04.123456 | 200 | {server=”MYSERVER”} |
| total_duration_requests | … 19:45:04.123456 | 2000 | {server=”MYSERVER”} |
- There are metrics with labels total_count_requests and total_duration_requests.
- The labels can refer to different applications, such as jobname=MYJOB, and server=MYSERVER
- For jobname=MYJOB, there are requests=database, and data=webserver.
Focusing on the data you want
If you display “total_count_requests”, you will get a graph showing the data for all total_count_requests data, including both jobname=”MYJOB” and server=”MYSERVER”
You can use
total_count_requests{jobname="MYJOB"}
This will display only the total count requests where jobname=”MYJOB”. In the chart you will get a layer for each unique combination of attributes, so a layer for requests=”database” and a layer for requests=”webserver”.
You could use
total_count_requests{jobname="MYJOB",requests="database"}
to further restrict what is displayed.
Interpreting the data
I am working with metrics traces_span_metrics_duration_milliseconds_sum, and traces_span_metrics_duration_milliseconds_count. I’ll shorten these to _sum and _count.
If you have two data values, count of requests, and duration of requests, you can calculate the average time per requests: _sum/_count.
If these values are cumulative, that is they are not reset periodically, then you need to be careful how you interpret the data. The average value will be the average value of all requests. Overnight when there are no requests being processed, the average value remains constant.
What people are more interested in is the change between two points in time, and calculating the average.
You can aggregate the data such as _sum[1m] and _count[1m] which returns the sum of the data values in the 1 minute interval.
If _sum[1m] has range values [20,40,60], and _count[1m] has range values [1,2,5] then _sum[1m] / _count[1m] is iterate over the value, _sum[i]/_count[i], which produces [20,20,12]
You can use the increase function which takes two sets of timestamp records.
increase(traces_span_metrics_duration_milliseconds_sum[1m])
/
increase(traces_span_metrics_duration_milliseconds_count[1m])
This says take the _sum data, and put it into time buckets 1 minute wide, then calculate the increase in the value of each bucket
- increase(traces_span_metrics_duration_milliseconds_sum[1m]) returns an array of the increase the sum of the time in each 1 minute time interval.
- increase(traces_span_metrics_duration_milliseconds_count[1m]) returns an array of the count of requests that happened each 1 minute time interval.
The increase(_sum[1m])/increase(_count[1m]) shows the average time for each request in each 1 minute windows.
If you want to find the average for all requests
sum(increase(traces_span_metrics_duration_milliseconds_sum[1m]))
/
sum(increase(traces_span_metrics_duration_milliseconds_count[1m]))
Note: You can also use
sum(rate(traces_span_metrics_duration_milliseconds_sum[1m]))
/
sum(rate(traces_span_metrics_duration_milliseconds_count[1m]))
- This is computed as ((delta(_sum)/1m) /((delta(_count)/1m) – the time interval cancels out to give delta(_sum)/delta(_count) which is the same as increase(…)/increase(…).
Displaying the sum(increase….)/sum(…) in Grafana, you can use explain query – and it gave
- sum(increase(traces_span_metrics_duration_milliseconds_sum[1m])) / (sum(increase(traces_span_metrics_duration_milliseconds_count[1m])))
- Fetch all series matching metric name and label filters.
- increase(<expr>[1m])
- Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.
- sum(<expr>)
- Calculates sum over the dimensions.
The basic data point
A data point has
- a metric name, such as “requestActiveTime”, “traceCount”, “traceSum”
- a timestamp – of when the data point was created
- a value – depending on the metric, it could be a string, a counter (which only increases), or a numeric value (which can go up and down)
- dimensions/tags/attributes/labels. Zero or more meta data keyword=values pairs, such as “jobName=MYJOB”, “activity=database”
The smallest database
| time t0 | time t1 | time t2 | |
| requestCount (task=”job1″) | 5 | 6 | 7 |
| requestCount (task=”job2″) | 7 | 9 | 10 |
| requestSum (task=”job1″) | 24 | 30 | 25 |
| requestSum(task=”job2″ | 30 | 50 | 60 |
Displaying data
Display all data
If you display requestCount in Prometheus
requestCount
you will get 3 data pairs: (time t0, 5+7), (time t1,6+9), (time t2,7+10). This is the data displayed for all requestCount records at that timestamp added together. Prometheus can display the data in a table, or in a graph.
Tools like Prometheus and Grafana, select a window of data, so you can select the last 5 minutes, the last hour, or specify a date-time range.
You can request a subset of information
You can select which rows you want
requestCount{task="job1"}
You will get 3 pairs of data items: (time t0,5), (time t1,6), (time t2,7) This is all of the rows for requestCount with task=”job1″ (in the current selected time range).
If you refresh the display, the data will gradually move left and fall off the screen, as the time stamps fall out of the current display window.
You can display groups of data
by (task) (requestCount)
Will produce 6 pairs of data times
- Job1: (time t0,5), (time t1,6), (time t2,7)
- job2: (time t0,7), (time t1,9), (time t2,10)
Applying functions
You can use
sum(requestCount)
sum by (task) (requestCount)
Instant values and range values
The data pairs above are called instance values – values from that particular instant in time. Some functions act on a range of time stamps. You can specify a timestamp range by specifying [5m] after the metric name, for a 5 minute range.
rate(requestCount{task="job1"}[5m])
with
| time t0 | time t1 | time t2 | |
| requestCount (task=”job1″) | 5 | 6 | 7 |
Where the above rate statement is the Per-second rate of increase, averaged over last 5 minutes. With rate() you pass in a time duration which covers a range of timestamps. Prometheus will process the data to match what you specify.
The documentation says for rate
rate(v range-vector)calculates the per-second average rate of increase of the time series in the range vector.
More complex data
You might have some data points with multiple dimensions “jobname=…” and “userid=…”. Prometheus will output a record for every unique set of dimensions.
You can use
by(task) (jobrequestCount)
will produce records for each unique task – and ignore any other dimensions.
You can explicitly ignore dimensions
without(userid) (jobrequestCount)
if there were many dimensions, only the userid dimension would be excluded from the displays.
You can do calculations on time ranges
You can do
requestSum/requestCount
or
requestSum[1m]/requestCount[1m]
The requestSum[1m]/requestCount[1m] produces an array where the timestamp increases every minute.
Handing lots of data
There may be many thousands of OTEL records produced a second, from one system. Keeping this data for many months means there could be a lot of data to be stored, and would lead to expensive processing at display time.
Data can be aggregated
A simple aggregation is for the front end OTEL records processing system, to produce every 30 seconds (for example) one record with the sum, and the count of records. This means you get one record every 30 seconds – compare to 10,000 individual records.
This may not provide the right level of information. A better way is the use of buckets, where each record is accumulated.
Some real data
startTime:...
endTime:...
"count": "64",
"sum": 73.024271,
"bucketCounts": [
"0",
"8",
"55",
"1",
"0",
"0",
"0",
"0"
],
"explicitBounds": [
0.1,
1,
2,
6,
10,
100,
250
],
...
You can calculate the average ( sum/count).
This has a range of buckets 0 ms to 0.1 ms, 0.1 to 1, 1 to 2 … and how many elements were in that bucket
You can plot the data and get a “profile” like

If the profile changes significantly over time, then you need to investigate why.
What 5 spans had longest time?
I found the data displayed had 20 layers in the graph – too many to be able to see and manage. When investigating problems, you are interested in the long times – and can ignore the small times. You can use the display the top k values using the topk function.
The query in Grafana which includes topk(Select largest k elements by sample value)
topk by(__name__) (5,
sum by(span_name) (increase(traces_span_metrics_duration_milliseconds_sum [1m]))) /
(sum by(span_name) (increase(traces_span_metrics_duration_milliseconds_count[1m]))
)
gave me

Where the yellow/brown is MQPUT RSERVER, and the purple is MQPUT to COLIN2.
It shows that some resources always took a long time(green), and some resources only occasionally took a longer time (yellow).










