-
Notifications
You must be signed in to change notification settings - Fork 0
/
Prometheus.jl
1144 lines (991 loc) · 35.6 KB
/
Prometheus.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# SPDX-License-Identifier: MIT
module Prometheus
using CodecZlib: GzipCompressorStream
using HTTP: HTTP
using SimpleBufferStream: BufferStream
abstract type Collector end
#########
# Utils #
#########
abstract type PrometheusException <: Exception end
struct ArgumentError <: PrometheusException
msg::String
end
function Base.showerror(io::IO, err::ArgumentError)
print(io, "Prometheus.", nameof(typeof(err)), ": ", err.msg)
end
struct AssertionError <: PrometheusException
msg::String
end
macro assert(cond)
msg = string(cond)
return :($(esc(cond)) || throw(AssertionError($msg)))
end
function Base.showerror(io::IO, err::AssertionError)
print(
io,
"Prometheus.AssertionError: `", err.msg, "`. This is unexpected, please file an " *
"issue at https://github.com/fredrikekre/Prometheus.jl/issues/new.",
)
end
# https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
# Metric names may contain ASCII letters, digits, underscores, and colons.
# It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*.
# Note: The colons are reserved for user defined recording rules. They should
# not be used by exporters or direct instrumentation.
function verify_metric_name(metric_name::String)
metric_name_regex = r"^[a-zA-Z_:][a-zA-Z0-9_:]*$"
if !occursin(metric_name_regex, metric_name)
throw(ArgumentError("metric name \"$(metric_name)\" is invalid"))
end
return metric_name
end
###########################################
# Compat for const fields, @lock, @atomic #
###########################################
@eval macro $(Symbol("const"))(field)
if VERSION >= v"1.8.0-DEV.1148"
Expr(:const, esc(field))
else
return esc(field)
end
end
if VERSION < v"1.7.0"
# Defined but not exported
using Base: @lock
end
if !isdefined(Base, Symbol("@atomic")) # v1.7.0
const ATOMIC_COMPAT_LOCK = ReentrantLock()
macro atomic(expr)
if Meta.isexpr(expr, :(::))
return esc(expr)
else
return quote
lock(ATOMIC_COMPAT_LOCK)
tmp = $(esc(expr))
unlock(ATOMIC_COMPAT_LOCK)
tmp
end
end
end
end
if !isdefined(Base, :eachsplit) # v1.8.0
const eachsplit = split
end
#####################
# CollectorRegistry #
#####################
struct CollectorRegistry
lock::ReentrantLock
collectors::Base.IdSet{Collector}
function CollectorRegistry()
return new(ReentrantLock(), Base.IdSet{Collector}())
end
end
function register(reg::CollectorRegistry, collector::Collector)
existing_names = Set{String}() # TODO: Cache existing_names in the registry?
@lock reg.lock begin
for c in reg.collectors
union!(existing_names, metric_names(c))
end
for metric_name in metric_names(collector)
if metric_name in existing_names
throw(ArgumentError(
"collector already contains a metric with the name \"$(metric_name)\""
))
end
end
push!(reg.collectors, collector)
end
return
end
function unregister(reg::CollectorRegistry, collector::Collector)
@lock reg.lock delete!(reg.collectors, collector)
return
end
##############
# Collectors #
##############
# abstract type Collector end
function collect(collector::Collector)
return collect!(Metric[], collector)
end
########################
# Counter <: Collector #
########################
# https://prometheus.io/docs/instrumenting/writing_clientlibs/#counter
# TODO: A counter is ENCOURAGED to have:
# - A way to count exceptions throw/raised in a given piece of code, and optionally only
# certain types of exceptions. This is count_exceptions in Python.
mutable struct Counter <: Collector
@const metric_name::String
@const help::String
@atomic value::Float64
function Counter(
metric_name::String, help::String;
registry::Union{CollectorRegistry, Nothing}=DEFAULT_REGISTRY,
)
initial_value = 0.0
counter = new(verify_metric_name(metric_name), help, initial_value)
if registry !== nothing
register(registry, counter)
end
return counter
end
end
"""
Prometheus.Counter(name, help; registry=DEFAULT_REGISTRY)
Construct a Counter collector.
**Arguments**
- `name :: String`: the name of the counter metric.
- `help :: String`: the documentation for the counter metric.
**Keyword arguments**
- `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.inc`](@ref): increment the counter.
"""
Counter(::String, ::String; kwargs...)
function metric_names(counter::Counter)
return (counter.metric_name, )
end
"""
Prometheus.inc(counter::Counter, v::Real = 1)
Increment the value of the counter with `v`. The value defaults to `v = 1`.
Throw a `Prometheus.ArgumentError` if `v < 0` (a counter must not decrease).
"""
function inc(counter::Counter, v::Real = 1.0)
if v < 0
throw(ArgumentError(
"invalid value $v: a counter must not decrease"
))
end
@atomic counter.value += convert(Float64, v)
return nothing
end
function collect!(metrics::Vector, counter::Counter)
push!(metrics,
Metric(
"counter", counter.metric_name, counter.help,
Sample(nothing, nothing, nothing, @atomic(counter.value)),
),
)
return metrics
end
######################
# Gauge <: Collector #
######################
# https://prometheus.io/docs/instrumenting/writing_clientlibs/#gauge
mutable struct Gauge <: Collector
@const metric_name::String
@const help::String
@atomic value::Float64
function Gauge(
metric_name::String, help::String;
registry::Union{CollectorRegistry, Nothing}=DEFAULT_REGISTRY,
)
initial_value = 0.0
gauge = new(verify_metric_name(metric_name), help, initial_value)
if registry !== nothing
register(registry, gauge)
end
return gauge
end
end
"""
Prometheus.Gauge(name, help; registry=DEFAULT_REGISTRY)
Construct a Gauge collector.
**Arguments**
- `name :: String`: the name of the gauge metric.
- `help :: String`: the documentation for the gauge metric.
**Keyword arguments**
- `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.inc`](@ref inc(::Gauge, ::Real)): increment the value
of the gauge.
- [`Prometheus.dec`](@ref): decrement the value of the gauge.
- [`Prometheus.set`](@ref): set the value of the gauge.
- [`Prometheus.set_to_current_time`](@ref): set the value of the gauge to the
current unixtime.
- [`Prometheus.@time`](@ref): time a section and set the value of the the gauge to the
elapsed time.
- [`Prometheus.@inprogress`](@ref): Track number of inprogress operations; increment the
gauge when entering the section, decrement it when leaving.
"""
Gauge(::String, ::String; kwargs...)
function metric_names(gauge::Gauge)
return (gauge.metric_name, )
end
"""
Prometheus.inc(gauge::Gauge, v::Real = 1)
Increment the value of the gauge with `v`.
`v` defaults to `v = 1`.
"""
function inc(gauge::Gauge, v::Real = 1.0)
@atomic gauge.value += convert(Float64, v)
return nothing
end
"""
Prometheus.dec(gauge::Gauge, v::Real = 1)
Decrement the value of the gauge with `v`.
`v` defaults to `v = 1`.
"""
function dec(gauge::Gauge, v::Real = 1.0)
@atomic gauge.value -= convert(Float64, v)
return nothing
end
"""
Prometheus.set(gauge::Gauge, v::Real)
Set the value of the gauge to `v`.
"""
function set(gauge::Gauge, v::Real)
@atomic gauge.value = convert(Float64, v)
return nothing
end
"""
Prometheus.set_to_current_time(gauge::Gauge)
Set the value of the gauge to the current unixtime in seconds.
"""
function set_to_current_time(gauge::Gauge)
@atomic gauge.value = time()
return nothing
end
function collect!(metrics::Vector, gauge::Gauge)
push!(metrics,
Metric(
"gauge", gauge.metric_name, gauge.help,
Sample(nothing, nothing, nothing, @atomic(gauge.value)),
),
)
return metrics
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::Real)
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::Real)
v = convert(Float64, 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 #
########################
# https://prometheus.io/docs/instrumenting/writing_clientlibs/#summary
mutable struct Summary <: Collector
@const metric_name::String
@const help::String
@atomic _count::Int
@atomic _sum::Float64
function Summary(
metric_name::String, help::String;
registry::Union{CollectorRegistry, Nothing}=DEFAULT_REGISTRY,
)
initial_count = 0
initial_sum = 0.0
summary = new(verify_metric_name(metric_name), help, initial_count, initial_sum)
if registry !== nothing
register(registry, summary)
end
return summary
end
end
"""
Prometheus.Summary(name, help; registry=DEFAULT_REGISTRY)
Construct a Summary collector.
**Arguments**
- `name :: String`: the name of the summary metric.
- `help :: String`: the documentation for the summary metric.
**Keyword arguments**
- `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 observe(::Summary, ::Real)): add an observation to the
summary.
- [`Prometheus.@time`](@ref): time a section and add the elapsed time as an observation.
"""
Summary(::String, ::String; kwargs...)
function metric_names(summary::Summary)
return (summary.metric_name * "_count", summary.metric_name * "_sum")
end
"""
Prometheus.observe(summary::Summary, v::Real)
Add the observed value `v` to the summary.
This increases the sum and count of the summary with `v` and `1`, respectively.
"""
function observe(summary::Summary, v::Real)
@atomic summary._count += 1
@atomic summary._sum += convert(Float64, v)
return nothing
end
function collect!(metrics::Vector, summary::Summary)
push!(metrics,
Metric(
"summary", summary.metric_name, summary.help,
[
Sample("_count", nothing, nothing, @atomic(summary._count)),
Sample("_sum", nothing, nothing, @atomic(summary._sum)),
]
),
)
return metrics
end
################
# "Decorators" #
################
"""
Prometheus.@time collector expr
Time the evaluation of `expr` and send the elapsed time in seconds to `collector`. The
specific action depends on the type of collector:
- `collector :: Gauge`: set the value of the gauge to the elapsed time
([`Prometheus.set`](@ref))
- `collector :: Histogram` and `collector :: Summary`: add the elapsed time as an
observation ([`Prometheus.observe`](@ref))
The expression to time, `expr`, can be a single expression (for example a function call), or
a code block (`begin`, `let`, etc), e.g.
```julia
Prometheus.@time collector <expr>
Prometheus.@time collector begin
<expr>
end
```
It is also possible to apply the macro to a function *definition*, i.e.
```julia
Prometheus.@time collector function time_this(args...)
# function body
end
```
to time every call to this function (covering all call sites).
"""
macro time(collector, expr)
return expr_gen(:time, collector, expr)
end
at_time(gauge::Gauge, v) = set(gauge, v)
at_time(summary::Summary, v) = observe(summary, v)
at_time(histogram::Histogram, v) = observe(histogram, v)
"""
Prometheus.@inprogress collector expr
Track the number of concurrent in-progress evaluations of `expr`. From the builtin
collectors this is only applicable to the [`Gauge`](@ref) -- the value of the gauge is
incremented with 1 when entering the section, and decremented with 1 when exiting the
section.
The expression, `expr`, can be a single expression (for example a function call), or a code
block (`begin`, `let`, etc), e.g.
```julia
Prometheus.@inprogress collector <expr>
Prometheus.@inprogress collector begin
<expr>
end
```
It is also possible to apply the macro to a function *definition*, i.e.
```julia
Prometheus.@inprogress collector function track_this(args...)
# function body
end
```
to track number of concurrent in-progress calls (covering all call sites).
"""
macro inprogress(collector, expr)
return expr_gen(:inprogress, collector, expr)
end
at_inprogress_enter(gauge::Gauge) = inc(gauge)
at_inprogress_exit(gauge::Gauge) = dec(gauge)
function expr_gen(macroname, collector, code)
if macroname === :time
local cllctr, t0, val
@gensym cllctr t0 val
preamble = Expr[
Expr(:(=), cllctr, esc(collector)),
Expr(:(=), t0, Expr(:call, time)),
]
postamble = Expr[
Expr(:(=), val, Expr(:call, max, Expr(:call, -, Expr(:call, time), t0), 0.0)),
Expr(:call, at_time, cllctr, val)
]
elseif macroname === :inprogress
local cllctr
@gensym cllctr
preamble = Expr[
Expr(:(=), cllctr, esc(collector)),
Expr(:call, at_inprogress_enter, cllctr),
]
postamble = Expr[
Expr(:call, at_inprogress_exit, cllctr)
]
else
throw(ArgumentError("unknown macro name $(repr(macroname))"))
end
local ret
@gensym ret
if Meta.isexpr(code, :function) || Base.is_short_function_def(code)
@assert length(code.args) == 2
fsig = esc(code.args[1])
fbody = esc(code.args[2])
return Expr(
code.head, # might as well preserve :function or :(=)
fsig,
Expr(
:block,
preamble...,
Expr(
:tryfinally,
Expr(:(=), ret, fbody),
Expr(:block, postamble...,),
),
ret,
),
)
else
return Expr(
:block,
preamble...,
Expr(
:tryfinally,
Expr(:(=), ret, esc(code)),
Expr(:block, postamble...,),
),
ret,
)
end
end
####################################
# Family{<:Collector} <: Collector #
####################################
# https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
# - Labels may contain ASCII letters, numbers, as well as underscores.
# They must match the regex [a-zA-Z_][a-zA-Z0-9_]*.
# - Label names beginning with __ (two "_") are reserved for internal use.
function verify_label_name(label_name::String)
label_name_regex = r"^[a-zA-Z_][a-zA-Z0-9_]*$"
if !occursin(label_name_regex, label_name) || startswith(label_name, "__")
throw(ArgumentError("label name \"$(label_name)\" is invalid"))
end
return label_name
end
struct LabelNames{N}
label_names::NTuple{N, Symbol}
function LabelNames(label_names::NTuple{N, Symbol}) where N
for label_name in label_names
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
h = hash(v, h)
end
return h
end
function Base.:(==)(l1::LabelValues, l2::LabelValues)
return l1.label_values == l2.label_values
end
struct Family{C, N, F} <: Collector
metric_name::String
help::String
label_names::LabelNames{N}
children::Dict{LabelValues{N}, C}
lock::ReentrantLock
constructor::F
function Family{C}(
metric_name::String, help::String, args_first, args_tail...;
registry::Union{CollectorRegistry, Nothing}=DEFAULT_REGISTRY, kwargs...,
) where {C}
# Support ... on non-final argument
args_all = (args_first, args_tail...,)
label_names = last(args_all)
args = Base.front(args_all)
@assert(isempty(args))
# TODO: Perhaps extract this into
# make_constructor(::Type{Collector}, metric_name, help, args...; kwargs...)
# so that some Collectors (like Counter) can skip the closure over args and kwargs.
function constructor()
return C(metric_name, help, args...; kwargs..., registry=nothing)::C
end
labels = LabelNames(label_names)
N = length(labels.label_names)
children = Dict{LabelValues{N}, C}()
lock = ReentrantLock()
family = new{C, N, typeof(constructor)}(
verify_metric_name(metric_name), help, labels, children, lock, constructor,
)
if registry !== nothing
register(registry, family)
end
return family
end
end
"""
Prometheus.Family{C}(name, help, args..., label_names; registry=DEFAULT_REGISTRY, kwargs...)
Create a labeled collector family with labels given by `label_names`. For every new set of
label values encountered a new collector of type `C <: Collector` will be created, see
[`Prometheus.labels`](@ref).
**Arguments**
- `name :: String`: the name of the family metric.
- `help :: String`: the documentation for the family metric.
- `args...`: any extra positional arguments required for `C`s constructor, see
[`Prometheus.labels`](@ref).
- `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
collector. If not specified the default registry is used. Pass `registry = nothing` to
skip registration.
- `kwargs...`: any extra keyword arguments required for `C`s constructor, see
[`Prometheus.labels`](@ref).
**Methods**
- [`Prometheus.labels`](@ref): get or create the collector for a specific set of labels.
- [`Prometheus.remove`](@ref): remove the collector for a specific set of labels.
- [`Prometheus.clear`](@ref): remove all collectors in the family.
# Examples
```julia
# Construct a family of Counters
counter_family = Prometheus.Family{Counter}(
"http_requests", "Number of HTTP requests", (:target, :status_code),
)
# 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
function metric_names(family::Family)
return (family.metric_name, )
end
"""
Prometheus.labels(family::Family{C}, label_values) where C
Get or create the collector of type `C` from the family corresponding to the labels given by
`label_values`. If no collector exist for the input labels a new one is created by invoking
the `C` constructor as `C(name, help, args...; kwargs..., registry=nothing)`, where `name`,
`help`, `args...`, and `kwargs...` are the arguments from the family constructor, see
[`Family`](@ref).
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`.
!!! tip
`Base.getindex` is overloaded to have the meaning of `Prometheus.labels` for the family
collector: `family[label_values]` is equivalent to
`Prometheus.labels(family, label_values)`.
!!! 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) where {C, N}
labels = make_label_values(family.label_names, label_values)::LabelValues{N}
collector = @lock family.lock get!(family.children, labels) do
family.constructor()::C
end
return collector
end
# Support family[labels] as a cute way of extracting the collector
function Base.getindex(family::Family, label_values)
return labels(family, label_values)
end
"""
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) where N
labels = make_label_values(family.label_names, label_values)::LabelValues{N}
@lock family.lock delete!(family.children, labels)
return
end
"""
Prometheus.clear(family::Family)
Remove all collectors in the family. Effectively this resets the collectors since
[`Prometheus.labels`](@ref) will recreate them when needed.
!!! note
This method invalidates all cached collectors.
"""
function clear(family::Family)
@lock family.lock empty!(family.children)
return
end
prometheus_type(::Type{Counter}) = "counter"
prometheus_type(::Type{Gauge}) = "gauge"
prometheus_type(::Type{Histogram}) = "histogram"
prometheus_type(::Type{Summary}) = "summary"
function collect!(metrics::Vector, family::Family{C}) where C
type = prometheus_type(C)
samples = Sample[]
buf = Metric[]
label_names = family.label_names
@lock family.lock begin
for (label_values, child) in family.children
# collect!(...) the child, throw away the metric, but keep the samples
child_metrics = collect!(resize!(buf, 0), child)
@assert length(child_metrics) == 1 # TODO: maybe this should be supported?
child_metric = child_metrics[1]
@assert(child_metric.type == type)
# Unwrap and rewrap samples with the labels
child_samples = child_metric.samples
if child_samples isa Sample
push!(samples, Sample(child_samples.suffix, label_names, label_values, child_samples.value))
else
@assert(child_samples isa Vector{Sample})
for child_sample in child_samples
if C === Histogram && (child_sample.label_names !== nothing) && (child_sample.label_values !== nothing)
# 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
# Sort samples lexicographically by the labels
sort!(samples; by = function(x)
labels = x.label_values
@assert(labels !== nothing)
return labels.label_values
end)
push!(
metrics,
Metric(type, family.metric_name, family.help, samples),
)
return metrics
end
##############
# Exposition #
##############
struct Sample
suffix::Union{String, Nothing} # e.g. _count or _sum
label_names::Union{LabelNames, Nothing}
label_values::Union{LabelValues, Nothing}
value::Float64
function Sample(
suffix::Union{String, Nothing},
label_names::Union{Nothing, LabelNames{N}},
label_values::Union{Nothing, LabelValues{N}},
value::Real,
) where N
@assert((label_names === nothing) === (label_values === nothing))
return new(suffix, label_names, label_values, value)
end
end
struct Metric
type::String
metric_name::String
help::String
# TODO: Union{Tuple{Sample}, Vector{Sample}} would always make this iterable.
samples::Union{Sample, Vector{Sample}}
end
function print_escaped(io::IO, help::String, esc)
for c in help
if c in esc
c == '\n' ? print(io, "\\n") : print(io, '\\', c)
else
print(io, c)
end
end
return
end
function expose_metric(io::IO, metric::Metric)
print(io, "# HELP ", metric.metric_name, " ")
print_escaped(io, metric.help, ('\\', '\n'))
println(io)
println(io, "# TYPE ", metric.metric_name, " ", metric.type)
samples = metric.samples
if samples isa Sample
# Single sample, no labels
@assert(samples.label_names === nothing)
@assert(samples.label_values === nothing)
@assert(samples.suffix === nothing)
val = samples.value
println(io, metric.metric_name, " ", isinteger(val) ? Int(val) : val)
else