Browse Source

Histogram: fourth and final basic collector type (#10)

pull/11/head
Fredrik Ekre 2 years ago committed by GitHub
parent
commit
437709c020
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 23
      docs/src/index.md
  3. 148
      src/Prometheus.jl
  4. 180
      test/runtests.jl

5
CHANGELOG.md

@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
<!-- ## [Unreleased] --> ## [Unreleased]
### Added
- The fourth basic collector, `Histogram`, have been added. ([#10][github-10])
## [1.1.0] - 2023-11-13 ## [1.1.0] - 2023-11-13
### Added ### Added
@ -47,6 +49,7 @@ See [README.md](README.md) for details and documentation.
[github-6]: https://github.com/fredrikekre/Prometheus.jl/pull/6 [github-6]: https://github.com/fredrikekre/Prometheus.jl/pull/6
[github-7]: https://github.com/fredrikekre/Prometheus.jl/pull/7 [github-7]: https://github.com/fredrikekre/Prometheus.jl/pull/7
[github-10]: https://github.com/fredrikekre/Prometheus.jl/pull/10
[Unreleased]: https://github.com/fredrikekre/Prometheus.jl/compare/v1.1.0...HEAD [Unreleased]: https://github.com/fredrikekre/Prometheus.jl/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/fredrikekre/Prometheus.jl/compare/v1.0.1...v1.1.0 [1.1.0]: https://github.com/fredrikekre/Prometheus.jl/compare/v1.0.1...v1.1.0

23
docs/src/index.md

@ -61,9 +61,9 @@ exposed.
## Collectors ## Collectors
This section documents the collectors that are currently supported. This include the "basic" This section documents the collectors that are currently supported. This include the "basic"
collectors ([Counter](@ref), [Gauge](@ref), [Summary](@ref)) as well as some custom collectors ([Counter](@ref), [Gauge](@ref), [Histogram](@ref), [Summary](@ref)) as well as
collectors ([GCCollector](@ref), [ProcessCollector](@ref)). There is also a section on how some custom collectors ([GCCollector](@ref), [ProcessCollector](@ref)). There is also a
to implement your own collector, see [Custom collectors](@ref). section on how to implement your own collector, see [Custom collectors](@ref).
Upstream documentation: Upstream documentation:
- <https://prometheus.io/docs/concepts/metric_types/> - <https://prometheus.io/docs/concepts/metric_types/>
@ -111,6 +111,23 @@ Prometheus.@time
Prometheus.@inprogress Prometheus.@inprogress
``` ```
### Histogram
Quoting the [upstream
documentation](https://prometheus.io/docs/concepts/metric_types/#histogram):
> A histogram samples observations (usually things like request durations or response sizes)
> and counts them in configurable buckets. It also provides a sum of all observed values.
#### Histogram API reference
```@docs
Prometheus.Histogram(::String, ::String; kwargs...)
Prometheus.observe(::Prometheus.Histogram, ::Any)
```
```@docs; canonical=false
Prometheus.@time
```
### Summary ### Summary
Quoting the [upstream Quoting the [upstream

148
src/Prometheus.jl

@ -317,6 +317,119 @@ function collect!(metrics::Vector, gauge::Gauge)
end end
##########################
# Histogram <: Collector #
##########################
# https://prometheus.io/docs/instrumenting/writing_clientlibs/#histogram
# A histogram SHOULD have the same default buckets as other client libraries.
# https://github.com/prometheus/client_python/blob/d8306b7b39ed814f3ec667a7901df249cee8a956/prometheus_client/metrics.py#L565
const DEFAULT_BUCKETS = [
.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, Inf,
]
mutable struct Histogram <: Collector
@const metric_name::String
@const help::String
@const buckets::Vector{Float64}
@atomic _count::Int
@atomic _sum::Float64
@const bucket_counters::Vector{Threads.Atomic{Int}}
function Histogram(
metric_name::String, help::String; buckets::Vector{Float64}=DEFAULT_BUCKETS,
registry::Union{CollectorRegistry, Nothing}=DEFAULT_REGISTRY,
)
# Make a copy of and verify buckets
buckets = copy(buckets)
issorted(buckets) || throw(ArgumentError("buckets must be sorted"))
length(buckets) > 0 && buckets[end] != Inf && push!(buckets, Inf)
length(buckets) < 2 && throw(ArgumentError("must have at least two buckets"))
initial_sum = 0.0
initial_count = 0
bucket_counters = [Threads.Atomic{Int}(0) for _ in 1:length(buckets)]
histogram = new(
verify_metric_name(metric_name), help, buckets,
initial_count, initial_sum, bucket_counters,
)
if registry !== nothing
register(registry, histogram)
end
return histogram
end
end
"""
Prometheus.Histogram(name, help; buckets=DEFAULT_BUCKETS, registry=DEFAULT_REGISTRY)
Construct a Histogram collector.
**Arguments**
- `name :: String`: the name of the histogram metric.
- `help :: String`: the documentation for the histogram metric.
**Keyword arguments**
- `buckets :: Vector{Float64}`: the upper bounds for the histogram buckets. The buckets
must be sorted. `Inf` will be added as a last bucket if not already included. The default
buckets are `DEFAULT_BUCKETS = $(DEFAULT_BUCKETS)`.
- `registry :: Prometheus.CollectorRegistry`: the registry in which to register the
collector. If not specified the default registry is used. Pass `registry = nothing` to
skip registration.
**Methods**
- [`Prometheus.observe`](@ref): add an observation to the histogram.
- [`Prometheus.@time`](@ref): time a section and add the elapsed time as an observation.
"""
Histogram(::String, ::String; kwargs...)
function metric_names(histogram::Histogram)
return (
histogram.metric_name * "_count", histogram.metric_name * "_sum",
histogram.metric_name,
)
end
"""
Prometheus.observe(histogram::Histogram, v)
Add the observed value `v` to the histogram.
This increases the sum and count of the histogram with `v` and `1`, respectively, and
increments the counter for all buckets containing `v`.
"""
function observe(histogram::Histogram, v)
@atomic histogram._count += 1
@atomic histogram._sum += v
for (bucket, bucket_counter) in zip(histogram.buckets, histogram.bucket_counters)
# TODO: Iterate in reverse and break early
if v <= bucket
Threads.atomic_add!(bucket_counter, 1)
end
end
return nothing
end
function collect!(metrics::Vector, histogram::Histogram)
label_names = LabelNames(("le",))
push!(metrics,
Metric(
"histogram", histogram.metric_name, histogram.help,
[
Sample("_count", nothing, nothing, @atomic(histogram._count)),
Sample("_sum", nothing, nothing, @atomic(histogram._sum)),
(
Sample(
nothing, label_names,
make_label_values(label_names, (histogram.buckets[i],)),
histogram.bucket_counters[i][],
) for i in 1:length(histogram.buckets)
)...,
]
),
)
return metrics
end
######################## ########################
# Summary <: Collector # # Summary <: Collector #
######################## ########################
@ -357,7 +470,8 @@ Construct a Summary collector.
skip registration. skip registration.
**Methods** **Methods**
- [`Prometheus.observe`](@ref): add an observation to the summary. - [`Prometheus.observe`](@ref observe(::Summary, ::Any)): add an observation to the
summary.
- [`Prometheus.@time`](@ref): time a section and add the elapsed time as an observation. - [`Prometheus.@time`](@ref): time a section and add the elapsed time as an observation.
""" """
Summary(::String, ::String; kwargs...) Summary(::String, ::String; kwargs...)
@ -404,8 +518,8 @@ specific action depends on the type of collector:
- `collector :: Gauge`: set the value of the gauge to the elapsed time - `collector :: Gauge`: set the value of the gauge to the elapsed time
([`Prometheus.set`](@ref)) ([`Prometheus.set`](@ref))
- `collector :: Summary`: add the elapsed time as an observation - `collector :: Histogram` and `collector :: Summary`: add the elapsed time as an
([`Prometheus.observe`](@ref)) observation ([`Prometheus.observe`](@ref))
The expression to time, `expr`, can be a single expression (for example a function call), or The expression to time, `expr`, can be a single expression (for example a function call), or
a code block (`begin`, `let`, etc), e.g. a code block (`begin`, `let`, etc), e.g.
@ -431,6 +545,7 @@ end
at_time(gauge::Gauge, v) = set(gauge, v) at_time(gauge::Gauge, v) = set(gauge, v)
at_time(summary::Summary, v) = observe(summary, v) at_time(summary::Summary, v) = observe(summary, v)
at_time(histogram::Histogram, v) = observe(histogram, v)
at_time(::Collector, v) = unreachable() at_time(::Collector, v) = unreachable()
""" """
@ -760,6 +875,7 @@ end
prometheus_type(::Type{Counter}) = "counter" prometheus_type(::Type{Counter}) = "counter"
prometheus_type(::Type{Gauge}) = "gauge" prometheus_type(::Type{Gauge}) = "gauge"
prometheus_type(::Type{Histogram}) = "histogram"
prometheus_type(::Type{Summary}) = "summary" prometheus_type(::Type{Summary}) = "summary"
prometheus_type(::Type) = unreachable() prometheus_type(::Type) = unreachable()
@ -782,8 +898,30 @@ function collect!(metrics::Vector, family::Family{C}) where C
else else
@assert(child_samples isa Vector{Sample}) @assert(child_samples isa Vector{Sample})
for child_sample in child_samples for child_sample in child_samples
@assert(child_sample.label_values === nothing) if C === Histogram && (child_sample.label_names !== nothing) && (child_sample.label_values !== nothing)
push!(samples, Sample(child_sample.suffix, label_names, label_values, child_sample.value)) # TODO: Only allow child samples to be labeled for Histogram
# collectors for now.
@assert(
length(child_sample.label_names.label_names) ==
length(child_sample.label_values.label_values)
)
# TODO: Bypass constructor verifications
merged_names = LabelNames((
label_names.label_names...,
child_sample.label_names.label_names...,
))
merged_values = LabelValues((
label_values.label_values...,
child_sample.label_values.label_values...,
))
push!(samples, Sample(child_sample.suffix, merged_names, merged_values, child_sample.value))
else
@assert(
(child_sample.label_names === nothing) ===
(child_sample.label_values === nothing)
)
push!(samples, Sample(child_sample.suffix, label_names, label_values, child_sample.value))
end
end end
end end
end end

180
test/runtests.jl

@ -167,6 +167,76 @@ end
""" """
end end
@testset "Prometheus.Histogram" begin
# Constructors and implicit registration
empty!(Prometheus.DEFAULT_REGISTRY.collectors)
c = Prometheus.Histogram("metric_name_histogram", "A histogram.")
@test c in Prometheus.DEFAULT_REGISTRY.collectors
r = Prometheus.CollectorRegistry()
c = Prometheus.Histogram("metric_name_histogram", "A histogram."; registry=r)
@test c in r.collectors
@test c.buckets == Prometheus.DEFAULT_BUCKETS
@test c._count == 0
@test c._sum == 0
@test all(x -> x[] == 0, c.bucket_counters)
@test_throws(
Prometheus.ArgumentError("metric name \"invalid-name\" is invalid"),
Prometheus.Histogram("invalid-name", "help"),
)
# Prometheus.observe(...)
v1 = 0.9
Prometheus.observe(c, v1)
@test c._count == 1
@test c._sum == v1
for (ub, counter, known_count) in zip(c.buckets, c.bucket_counters, [zeros(Int, 9); ones(Int, 6)])
@test counter[] == (v1 > ub ? 0 : 1) == known_count
end
v2 = 10v1
Prometheus.observe(c, v2)
@test c._count == 2
@test c._sum == v1 + v2
for (ub, counter, known_count) in zip(c.buckets, c.bucket_counters, [zeros(Int, 9); [1, 1, 1, 1]; [2, 2]])
@test counter[] == ((v2 > ub && v1 > ub) ? 0 : v2 > ub ? 1 : 2) == known_count
end
# Prometheus.collect(...)
r = Prometheus.CollectorRegistry()
buckets = [1.0, 2.0, Inf]
c = Prometheus.Histogram("metric_name_histogram", "A histogram."; buckets=buckets, registry=r)
Prometheus.observe(c, 0.5)
Prometheus.observe(c, 1.6)
metrics = Prometheus.collect(c)
@test length(metrics) == 1
metric = metrics[1]
@test metric.metric_name == c.metric_name
@test metric.help == c.help
@test length(metric.samples) == length(buckets) + 2
s1, s2 = metric.samples[1], metric.samples[2]
@test s1.suffix == "_count"
@test s2.suffix == "_sum"
@test s1.label_values === nothing
@test s2.label_values === nothing
@test s1.value == 2
@test s2.value == 0.5 + 1.6
for (ub, counter, sample, known_count) in zip(c.buckets, c.bucket_counters, metric.samples[3:end], [1, 2, 2])
@test sample.suffix === nothing
@test (sample.label_names::Prometheus.LabelNames{1}).label_names === (:le,)
@test (sample.label_values::Prometheus.LabelValues{1}).label_values == (string(ub),)
@test sample.value == counter[] == known_count
end
# Prometheus.expose_metric(...)
@test sprint(Prometheus.expose_metric, metric) ==
sprint(Prometheus.expose_io, r) ==
"""
# HELP metric_name_histogram A histogram.
# TYPE metric_name_histogram histogram
metric_name_histogram_count 2
metric_name_histogram_sum 2.1
metric_name_histogram{le="1.0"} 1
metric_name_histogram{le="2.0"} 2
metric_name_histogram{le="Inf"} 2
"""
end
@testset "Prometheus.LabelNames and Prometheus.LabelValues" begin @testset "Prometheus.LabelNames and Prometheus.LabelValues" begin
@test_throws( @test_throws(
Prometheus.ArgumentError("label name \"invalid-label\" is invalid"), Prometheus.ArgumentError("label name \"invalid-label\" is invalid"),
@ -277,32 +347,46 @@ end
@test 0.3 > gauge.value > 0.1 @test 0.3 > gauge.value > 0.1
end end
@testset "Prometheus.@time summary::Summary" begin @testset "Prometheus.@time collector::$(Collector)" for Collector in (Prometheus.Histogram, Prometheus.Summary)
summary = Prometheus.Summary("call_time", "Time of calls"; registry=nothing) ishist = Collector === Prometheus.Histogram
Prometheus.@time summary sleep(0.1) buckets = [1.0, Inf]
@test 0.3 > summary._sum > 0.1 collector = Collector(
@test summary._count == 1 "call_time", "Time of calls";
Prometheus.@time summary let (ishist ? (; buckets=buckets) : (;))...,
registry=nothing,
)
Prometheus.@time collector sleep(0.1)
@test 0.3 > collector._sum > 0.1
@test collector._count == 1
ishist && @test (x->x[]).(collector.bucket_counters) == [1, 1]
Prometheus.@time collector let
sleep(0.1) sleep(0.1)
end end
@test 0.4 > summary._sum > 0.2 @test 0.4 > collector._sum > 0.2
@test summary._count == 2 @test collector._count == 2
Prometheus.@time summary f() = sleep(0.1) ishist && @test (x->x[]).(collector.bucket_counters) == [2, 2]
Prometheus.@time collector f() = sleep(0.1)
@sync begin @sync begin
@async f() @async f()
@async f() @async f()
end end
@test 0.7 > summary._sum > 0.4 @test 0.7 > collector._sum > 0.4
@test summary._count == 4 @test collector._count == 4
Prometheus.@time summary function g() ishist && @test (x->x[]).(collector.bucket_counters) == [4, 4]
Prometheus.@time collector function g()
sleep(0.1) sleep(0.1)
end end
@sync begin @sync begin
@async g() @async g()
@async g() @async g()
end end
@test 0.9 > summary._sum > 0.6 @test 0.9 > collector._sum > 0.6
@test summary._count == 6 @test collector._count == 6
ishist && @test (x->x[]).(collector.bucket_counters) == [6, 6]
if ishist
Prometheus.@time collector sleep(1.1)
@test (x->x[]).(collector.bucket_counters) == [6, 7]
end
end end
@testset "Prometheus.@inprogress gauge::Gauge" begin @testset "Prometheus.@inprogress gauge::Gauge" begin
@ -353,11 +437,12 @@ end
@test_throws Prometheus.UnreachableError Prometheus.at_inprogress_exit(Coll()) @test_throws Prometheus.UnreachableError Prometheus.at_inprogress_exit(Coll())
end end
@testset "Prometheus.Family{Summary}" begin @testset "Prometheus.Family{$Collector}" for Collector in (Prometheus.Histogram, Prometheus.Summary)
r = Prometheus.CollectorRegistry() r = Prometheus.CollectorRegistry()
c = Prometheus.Family{Prometheus.Summary}( c = Prometheus.Family{Collector}(
"http_request_time", "Time to process requests.", "http_request_time", "Time to process requests.",
("endpoint", "status_code"); ("endpoint", "status_code");
(Collector === Prometheus.Histogram ? (; buckets = [2.0, Inf]) : (;))...,
registry = r, registry = r,
) )
@test c in r.collectors @test c in r.collectors
@ -390,25 +475,50 @@ end
metric = metrics[1] metric = metrics[1]
@test metric.metric_name == c.metric_name @test metric.metric_name == c.metric_name
@test metric.help == c.help @test metric.help == c.help
@test length(metric.samples) == 4 if Collector === Prometheus.Histogram
s1, s2, s3, s4 = metric.samples buckets = Prometheus.labels(c, l1).buckets
@test s1.label_values.label_values == s2.label_values.label_values == ("/bar/", "404") @test length(buckets) == length(Prometheus.labels(c, l2).buckets)
@test s3.label_values.label_values == s4.label_values.label_values == ("/foo/", "200") @test length(metric.samples) == 2 * (length(buckets) + 2)
@test s1.value == 2 # _count # _count and _sum samples
@test s2.value == 6.4 # _sum s1, s2, s5, s6 = metric.samples[[1, 2, 5, 6]]
@test s3.value == 2 # _count @test s1.label_values.label_values == s2.label_values.label_values == ("/bar/", "404")
@test s4.value == 4.6 # _sum @test s5.label_values.label_values == s6.label_values.label_values == ("/foo/", "200")
# Prometheus.expose_metric(...) @test s1.value == 2 # _count
@test sprint(Prometheus.expose_metric, metric) == @test s2.value == 6.4 # _sum
sprint(Prometheus.expose_io, r) == @test s5.value == 2 # _count
""" @test s6.value == 4.6 # _sum
# HELP http_request_time Time to process requests. # {le} samples
# TYPE http_request_time summary for (ls, subrange) in ((l1, 7:8), (l2, 3:4))
http_request_time_count{endpoint="/bar/",status_code="404"} 2 for (ub, counter, sample) in zip(buckets, Prometheus.labels(c, ls).bucket_counters, metric.samples[subrange])
http_request_time_sum{endpoint="/bar/",status_code="404"} 6.4 @test sample.suffix === nothing
http_request_time_count{endpoint="/foo/",status_code="200"} 2 @test (sample.label_names::Prometheus.LabelNames{3}).label_names ===
http_request_time_sum{endpoint="/foo/",status_code="200"} 4.6 (:endpoint, :status_code, :le)
""" @test (sample.label_values::Prometheus.LabelValues{3}).label_values ==
(ls..., string(ub))
@test sample.value == counter[]
end
end
else # Collector === Prometheus.Summary
@test length(metric.samples) == 4
s1, s2, s3, s4 = metric.samples
@test s1.label_values.label_values == s2.label_values.label_values == ("/bar/", "404")
@test s3.label_values.label_values == s4.label_values.label_values == ("/foo/", "200")
@test s1.value == 2 # _count
@test s2.value == 6.4 # _sum
@test s3.value == 2 # _count
@test s4.value == 4.6 # _sum
# Prometheus.expose_metric(...)
@test sprint(Prometheus.expose_metric, metric) ==
sprint(Prometheus.expose_io, r) ==
"""
# HELP http_request_time Time to process requests.
# TYPE http_request_time summary
http_request_time_count{endpoint="/bar/",status_code="404"} 2
http_request_time_sum{endpoint="/bar/",status_code="404"} 6.4
http_request_time_count{endpoint="/foo/",status_code="200"} 2
http_request_time_sum{endpoint="/foo/",status_code="200"} 4.6
"""
end
end end
@testset "Label types for Prometheus.Family{C}" begin @testset "Label types for Prometheus.Family{C}" begin

Loading…
Cancel
Save