Browse Source

Family{C}: Generalize labeling specification (#7)

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).
pull/8/head
Fredrik Ekre 2 years ago committed by GitHub
parent
commit
e27d602bff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      CHANGELOG.md
  2. 40
      docs/src/index.md
  3. 91
      src/Prometheus.jl
  4. 38
      test/runtests.jl

7
CHANGELOG.md

@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
concurrent evalutations of `<expr>`. Just like `Prometheus.@time`, valid `<expr>`s are concurrent evalutations of `<expr>`. Just like `Prometheus.@time`, valid `<expr>`s are
single expressions, blocks, and function definitions. See documentation for more details. single expressions, blocks, and function definitions. See documentation for more details.
([#6][github-6]) ([#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 ## [1.0.1] - 2023-11-06
### Fixed ### 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-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 [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 [1.0.1]: https://github.com/fredrikekre/Prometheus.jl/compare/v1.0.0...v1.0.1

40
docs/src/index.md

@ -168,12 +168,46 @@ RandomCollector
## Labels ## Labels
See <https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels> for details. Prometheus allows attaching labels to metrics, see the upstream documentation:
- <https://prometheus.io/docs/practices/naming/#labels>
- <https://prometheus.io/docs/practices/instrumentation/#use-labels>
- <https://prometheus.io/docs/practices/instrumentation/#do-not-overuse-labels>
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 ```@docs
Prometheus.Family{C}(::String, ::String, ::Any; kwargs...) where C Prometheus.Family{C}(::String, ::String, ::Any; kwargs...) where C
Prometheus.labels(::Prometheus.Family{C, N}, ::NTuple{N, String}) where {C, N} Prometheus.labels(::Prometheus.Family{C, N}, ::Any) where {C, N}
Prometheus.remove(::Prometheus.Family{C, N}, ::NTuple{N, String}) where {C, N} Prometheus.remove(::Prometheus.Family{<:Any, N}, ::Any) where {N}
Prometheus.clear(::Prometheus.Family) Prometheus.clear(::Prometheus.Family)
``` ```

91
src/Prometheus.jl

@ -545,19 +545,49 @@ function verify_label_name(label_name::String)
end end
struct LabelNames{N} struct LabelNames{N}
label_names::NTuple{N, String} label_names::NTuple{N, Symbol}
function LabelNames(label_names::NTuple{N, String}) where N function LabelNames(label_names::NTuple{N, Symbol}) where N
for label_name in label_names for label_name in label_names
verify_label_name(label_name) verify_label_name(String(label_name))
end end
return new{N}(label_names) return new{N}(label_names)
end end
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} struct LabelValues{N}
label_values::NTuple{N, String} label_values::NTuple{N, String}
end 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) function Base.hash(l::LabelValues, h::UInt)
h = hash(0x94a2d04ee9e5a55b, h) # hash("Prometheus.LabelValues") on Julia 1.9.3 h = hash(0x94a2d04ee9e5a55b, h) # hash("Prometheus.LabelValues") on Julia 1.9.3
for v in l.label_values for v in l.label_values
@ -578,13 +608,15 @@ struct Family{C, N} <: Collector
lock::ReentrantLock lock::ReentrantLock
function Family{C}( 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, 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}() children = Dict{LabelValues{N}, C}()
lock = ReentrantLock() lock = ReentrantLock()
family = new{C, N}( 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 if registry !== nothing
register(registry, family) register(registry, family)
@ -602,7 +634,21 @@ label values encountered a new collector of type `C <: Collector` will be create
**Arguments** **Arguments**
- `name :: String`: the name of the family metric. - `name :: String`: the name of the family metric.
- `help :: String`: the documentation for 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** **Keyword arguments**
- `registry :: Prometheus.CollectorRegistry`: the registry in which to register the - `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 ```julia
# Construct a family of Counters # Construct a family of Counters
counter_family = Prometheus.Family{Counter}( 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" # Increment the counter for the labels `target="/api"` and `status_code=200`
Prometheus.inc(Prometheus.labels(counter_family, ["200", "/api"])) Prometheus.inc(Prometheus.labels(counter_family, (target="/api", status_code=200)))
``` ```
""" """
Family{C}(::String, ::String, ::Any; kwargs...) where C Family{C}(::String, ::String, ::Any; kwargs...) where C
@ -632,19 +678,29 @@ function metric_names(family::Family)
end 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 Return the collector of type `C` from the family corresponding to the labels given by
`label_values`. `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 !!! note
This method does an acquire/release of a lock, and a dictionary lookup, to find the 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 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 matter (below 100ns for some basic benchmarks) but it is safe to cache the returned
collector if required. collector if required.
""" """
function labels(family::Family{C, N}, label_values::NTuple{N, String}) where {C, N} function labels(family::Family{C, N}, label_values) where {C, N}
collector = @lock family.lock get!(family.children, LabelValues(label_values)) do 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? # TODO: Avoid the re-verification of the metric name?
C(family.metric_name, family.help; registry=nothing) C(family.metric_name, family.help; registry=nothing)
end end
@ -652,17 +708,20 @@ function labels(family::Family{C, N}, label_values::NTuple{N, String}) where {C,
end 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 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 since [`Prometheus.labels`](@ref) will recreate the collector when called with the same
label names. label names.
Refer to [`Prometheus.labels`](@ref) for how to specify `label_values`.
!!! note !!! note
This method invalidates cached collectors for the label names. This method invalidates cached collectors for the label names.
""" """
function remove(family::Family{<:Any, N}, label_values::NTuple{N, String}) where N function remove(family::Family{<:Any, N}, label_values) where N
@lock family.lock delete!(family.children, LabelValues(label_values)) labels = make_label_values(family.label_names, label_values)::LabelValues{N}
@lock family.lock delete!(family.children, labels)
return return
end end

38
test/runtests.jl

@ -411,6 +411,44 @@ end
""" """
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 @testset "Prometheus.GCCollector" begin
r = Prometheus.CollectorRegistry() r = Prometheus.CollectorRegistry()
c = Prometheus.GCCollector(; registry=r) c = Prometheus.GCCollector(; registry=r)

Loading…
Cancel
Save