From 26037f01d5fde7f739171040c0bedffa910b1264 Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Sun, 12 Nov 2023 23:28:59 +0100 Subject: [PATCH] Family{C}: Generalize labeling specification This patch changes how label names and label values can be passed to the constructor `Prometheus.Family{C}` and `Prometheus.labels`. In particular, the label names are now stored as symbols internally. This enables using `getfield` when creating the corresponding label values. Concretely this enables using e.g. named tuples, and custom structs, as label names/values. For example, the following now works ```julia struct RequestLabels target::String status_code::Int end request_counter = Prometheus.Family{Prometheus.Counter}( "http_requests", "Total number of HTTP requests", RequestLabels ) counter = Prometheus.labels(request_counter, RequestLabels("/api", 200)) ``` In this example, the field names of the type, `RequestLabels`, are used as label names in the `Family{C}` constructor. When extracting the counter for a specific set of labels an instance of the struct is used. All non-string values are stringified using string (`status_code::Int` in the example above, for example). --- CHANGELOG.md | 7 ++++ docs/src/index.md | 40 +++++++++++++++++++-- src/Prometheus.jl | 91 ++++++++++++++++++++++++++++++++++++++--------- test/runtests.jl | 38 ++++++++++++++++++++ 4 files changed, 157 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb4066..61f535a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 concurrent evalutations of ``. Just like `Prometheus.@time`, valid ``s are single expressions, blocks, and function definitions. See documentation for more details. ([#6][github-6]) + - New ways to specify label names and label values in `Prometheus.Family{C}`. Label names + can now be passed to the constructor as i) a tuple of strings or symbols, ii) a named + tuple type (names used for label names), or iii) a custom struct type (field names used + for label names). Similarly, label values (passed to e.g. `Prometheus.labels`) can be + passed as i) tuple of strings, ii) named tuple, iii) struct instance. See documentation + for examples and more details. ([#7][github-7]) ## [1.0.1] - 2023-11-06 ### Fixed @@ -38,6 +44,7 @@ See [README.md](README.md) for details and documentation. [github-6]: https://github.com/fredrikekre/Prometheus.jl/pull/6 +[github-7]: https://github.com/fredrikekre/Prometheus.jl/pull/7 [Unreleased]: https://github.com/fredrikekre/Prometheus.jl/compare/v1.0.1...HEAD [1.0.1]: https://github.com/fredrikekre/Prometheus.jl/compare/v1.0.0...v1.0.1 diff --git a/docs/src/index.md b/docs/src/index.md index ad54f5f..d701d38 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -168,12 +168,46 @@ RandomCollector ## Labels -See for details. +Prometheus allows attaching labels to metrics, see the upstream documentation: + - + - + - + +In this package labeling of collectors is done with [`Prometheus.Family`](@ref). A collector +family consist of a number of regular collectors, the children, with unique labels. + +A concrete example is a HTTP request `Counter`, where we might also want to keep track of +the target resource and the status code of the request. Such instrumentation can be +implemented as follows + +```julia +# Custom label struct +struct RequestLabels + target::String + status_code::Int +end + +# Create the counter family +request_counter = Prometheus.Family{Prometheus.Counter}( + "http_requests", "Total number of HTTP requests", RequestLabels +) + +# Extract a Counter for a specific set of labels +counter = Prometheus.labels(request_counter, RequestLabels("/api", 200)) + +# Increment the counter +Prometheus.inc(counter) +``` + +Note that using a custom label struct is optional, refer to the constructor +[`Prometheus.Family`](@ref) and [`Prometheus.labels`](@ref) for alternative methods. + +### Family API reference ```@docs Prometheus.Family{C}(::String, ::String, ::Any; kwargs...) where C -Prometheus.labels(::Prometheus.Family{C, N}, ::NTuple{N, String}) where {C, N} -Prometheus.remove(::Prometheus.Family{C, N}, ::NTuple{N, String}) where {C, N} +Prometheus.labels(::Prometheus.Family{C, N}, ::Any) where {C, N} +Prometheus.remove(::Prometheus.Family{<:Any, N}, ::Any) where {N} Prometheus.clear(::Prometheus.Family) ``` diff --git a/src/Prometheus.jl b/src/Prometheus.jl index 316081f..916cdc3 100644 --- a/src/Prometheus.jl +++ b/src/Prometheus.jl @@ -545,19 +545,49 @@ function verify_label_name(label_name::String) end struct LabelNames{N} - label_names::NTuple{N, String} - function LabelNames(label_names::NTuple{N, String}) where N + label_names::NTuple{N, Symbol} + function LabelNames(label_names::NTuple{N, Symbol}) where N for label_name in label_names - verify_label_name(label_name) + verify_label_name(String(label_name)) end return new{N}(label_names) end end +# Tuple of strings +function LabelNames(label_names::NTuple{N, String}) where N + return LabelNames(map(Symbol, label_names)) +end + +# NamedTuple-type or a (user defined) struct +function LabelNames(::Type{T}) where T + return LabelNames(fieldnames(T)) +end + struct LabelValues{N} label_values::NTuple{N, String} end +function make_label_values(::LabelNames{N}, label_values::NTuple{N, String}) where N + return LabelValues(label_values) +end + +stringify(str::String) = str +stringify(str) = String(string(str))::String + +# Heterogeneous tuple +function make_label_values(::LabelNames{N}, label_values::Tuple{Vararg{<:Any, N}}) where N + return LabelValues(map(stringify, label_values)::NTuple{N, String}) +end + +# NamedTuple or a (user defined) struct +function make_label_values(label_names::LabelNames{N}, label_values) where N + t::NTuple{N, String} = ntuple(N) do i + stringify(getfield(label_values, label_names.label_names[i]))::String + end + return LabelValues{N}(t) +end + function Base.hash(l::LabelValues, h::UInt) h = hash(0x94a2d04ee9e5a55b, h) # hash("Prometheus.LabelValues") on Julia 1.9.3 for v in l.label_values @@ -578,13 +608,15 @@ struct Family{C, N} <: Collector lock::ReentrantLock function Family{C}( - metric_name::String, help::String, label_names::NTuple{N, String}; + metric_name::String, help::String, label_names; registry::Union{CollectorRegistry, Nothing}=DEFAULT_REGISTRY, - ) where {C, N} + ) where {C} + labels = LabelNames(label_names) + N = length(labels.label_names) children = Dict{LabelValues{N}, C}() lock = ReentrantLock() family = new{C, N}( - verify_metric_name(metric_name), help, LabelNames(label_names), children, lock, + verify_metric_name(metric_name), help, labels, children, lock, ) if registry !== nothing register(registry, family) @@ -602,7 +634,21 @@ label values encountered a new collector of type `C <: Collector` will be create **Arguments** - `name :: String`: the name of the family metric. - `help :: String`: the documentation for the family metric. - - `label_names :: Tuple{String, ...}`: the label names. + - `label_names`: the label names for the family. Label names can be given as either of the + following (typically matching the methods label values will be given later, see + [`Prometheus.labels`](@ref)): + - a tuple of symbols or strings, e.g. `(:target, :status_code)` or + `("target", "status_code")` + - a named tuple type, e.g. `@NamedTuple{target::String, status_code::Int}` where the + names are used as the label names + - a custom struct type, e.g. `RequestLabels` defined as + ```julia + struct RequestLabels + target::String + status_code::Int + end + ``` + where the field names are used for the label names. **Keyword arguments** - `registry :: Prometheus.CollectorRegistry`: the registry in which to register the @@ -618,11 +664,11 @@ label values encountered a new collector of type `C <: Collector` will be create ```julia # Construct a family of Counters counter_family = Prometheus.Family{Counter}( - "http_requests", "Number of HTTP requests", ["status_code", "endpoint"], + "http_requests", "Number of HTTP requests", (:target, :status_code), ) -# Increment the counter for the labels status_code = "200" and endpoint = "/api" -Prometheus.inc(Prometheus.labels(counter_family, ["200", "/api"])) +# Increment the counter for the labels `target="/api"` and `status_code=200` +Prometheus.inc(Prometheus.labels(counter_family, (target="/api", status_code=200))) ``` """ Family{C}(::String, ::String, ::Any; kwargs...) where C @@ -632,19 +678,29 @@ function metric_names(family::Family) end """ - Prometheus.labels(family::Family{C}, label_values::Tuple{String, ...}) where C + Prometheus.labels(family::Family{C}, label_values) where C Return the collector of type `C` from the family corresponding to the labels given by `label_values`. +Similarly to when creating the [`Family`](@ref), `label_values` can be given as either of +the following: + - a tuple, e.g. `("/api", 200)` + - a named tuple with names matching the label names, e.g.`(target="/api", status_code=200)` + - a struct instance with field names matching the label names , e.g. + `RequestLabels("/api", 200)` + +All non-string values (e.g. `200` in the examples above) are stringified using `string`. + !!! note This method does an acquire/release of a lock, and a dictionary lookup, to find the collector matching the label names. For typical applications this overhead does not matter (below 100ns for some basic benchmarks) but it is safe to cache the returned collector if required. """ -function labels(family::Family{C, N}, label_values::NTuple{N, String}) where {C, N} - collector = @lock family.lock get!(family.children, LabelValues(label_values)) do +function labels(family::Family{C, N}, label_values) where {C, N} + labels = make_label_values(family.label_names, label_values)::LabelValues{N} + collector = @lock family.lock get!(family.children, labels) do # TODO: Avoid the re-verification of the metric name? C(family.metric_name, family.help; registry=nothing) end @@ -652,17 +708,20 @@ function labels(family::Family{C, N}, label_values::NTuple{N, String}) where {C, end """ - Prometheus.remove(family::Family, label_values::Tuple{String, ...}) + Prometheus.remove(family::Family, label_values) Remove the collector corresponding to `label_values`. Effectively this resets the collector since [`Prometheus.labels`](@ref) will recreate the collector when called with the same label names. +Refer to [`Prometheus.labels`](@ref) for how to specify `label_values`. + !!! note This method invalidates cached collectors for the label names. """ -function remove(family::Family{<:Any, N}, label_values::NTuple{N, String}) where N - @lock family.lock delete!(family.children, LabelValues(label_values)) +function remove(family::Family{<:Any, N}, label_values) where N + labels = make_label_values(family.label_names, label_values)::LabelValues{N} + @lock family.lock delete!(family.children, labels) return end diff --git a/test/runtests.jl b/test/runtests.jl index 23aa4c1..3714bfe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -411,6 +411,44 @@ end """ end +@testset "Label types for Prometheus.Family{C}" begin + struct RequestLabels + target::String + status_code::Int + end + for fam in ( + # Constructor with NTuple{N, String} names + Prometheus.Family{Prometheus.Counter}( + "http_requests", "Total number of HTTP requests", ("target", "status_code"); + registry=nothing, + ), + # Constructor with NTuple{N, Symbol} names + Prometheus.Family{Prometheus.Counter}( + "http_requests", "Total number of HTTP requests", (:target, :status_code); + registry=nothing, + ), + # Constructor with NamedTuple type + Prometheus.Family{Prometheus.Counter}( + "http_requests", "Total number of HTTP requests", + @NamedTuple{target::String, status_code::Int}; + registry=nothing, + ), + # Constructor with custom struct + Prometheus.Family{Prometheus.Counter}( + "http_requests", "Total number of HTTP requests", RequestLabels; + registry=nothing, + ), + ) + @test Prometheus.labels(fam, ("/api", "200")) === + Prometheus.labels(fam, ("/api", 200)) === + Prometheus.labels(fam, (target="/api", status_code="200")) === + Prometheus.labels(fam, (target="/api", status_code=200)) === + Prometheus.labels(fam, (status_code="200", target="/api")) === + Prometheus.labels(fam, (status_code=200, target="/api")) === + Prometheus.labels(fam, RequestLabels("/api", 200)) + end +end + @testset "Prometheus.GCCollector" begin r = Prometheus.CollectorRegistry() c = Prometheus.GCCollector(; registry=r)