Below is a simple server for Prometheus metrics. The code also has classes to represent Prometheus counters and gauges.
package require TclOO # Helper for callback functions proc oo::Helpers::mycode {args} { list namespace inscope [uplevel 1 {namespace current}] [linsert $args 0 my] } # Base class for Prometheus metrics: # # type : type of the metric, one of: counter, gauge # name : name of the metric, no spaces allowed # help : help text for the metric # labels: set of labels for the metric oo::class create PrometheusMetric { variable type name help labels constructor {itype iname ihelp args} { set type $itype set name $iname set help $ihelp set labels $args } method format_help {} { return "\# HELP $name $help" } method format_type {} { return "\# TYPE $name $type" } method format_value {value} { set result "$name" if {[llength $labels]} { set L {} foreach {k v} $labels { lappend L "$k=\"$v\"" } append result "\{" append result [join $L ","] append result "\}" } append result " " $value return $result } } # Counter metric oo::class create PrometheusCounter { superclass PrometheusMetric variable counter constructor {iname ihelp args} { set counter 0 next counter $iname $ihelp {*}$args } method inc {{v 1}} { set counter [expr {$counter + $v}] } method metric {} { return "[my format_help]\n[my format_type]\n[my format_value $counter]\n" } } # Gauge metric oo::class create PrometheusGauge { superclass PrometheusMetric variable value constructor {iname ihelp args} { set value 0 next gauge $iname $ihelp {*}$args } method inc {{v 1}} { set value [expr {$value + $v}] } method dec {{v 1}} { set value [expr {$value - $v}] } method set_value {v} { set value $v } method metric {} { return "[my format_help]\n[my format_type]\n[my format_value $value]\n" } } # simple server the Prometheus scraper can use to get metrics oo::class create PrometheusMetricsServer { # Based on dustmote variable port metrics server constructor {iport imetrics} { set port $iport set metrics $imetrics set server [socket -server [mycode answer] $port] } destructor { close $server } method answer {socketChannel host2 port2} { fileevent $socketChannel readable [mycode process_request $socketChannel] } method process_request {socketChannel} { fconfigure $socketChannel -blocking 0 set gotLine [gets $socketChannel] if { [fblocked $socketChannel] } then {return} fileevent $socketChannel readable "" puts $gotLine fconfigure $socketChannel -translation binary -buffering full puts $socketChannel "HTTP/1.0 200 OK" puts $socketChannel "" foreach p $metrics { puts $socketChannel [$p metric] } close $socketChannel } }
The example below creates 2 counters and 2 gauges and starts a server on port 8043 Prometheus can scrape:
# Create some metrics set P {} lappend P [PrometheusCounter new counter1 "Help for c1" a 1 b 1] lappend P [PrometheusCounter new counter1 "Help for c1" a 1 b 2] lappend P [PrometheusGauge new gauge1 "Help for g1" a 2 b 1] lappend P [PrometheusGauge new gauge1 "Help for g1" a 2 b 2] # Create the metrics server PrometheusMetricsServer create s1 8043 $P # Update the metrics every second proc update_metrics {} { puts "Update metrics" foreach p $::P { switch -exact -- [info object class $p] { ::PrometheusCounter { $p inc [expr {rand()}] } ::PrometheusGauge { if {[expr {rand()}] >= 0.5} { $p inc [expr {rand()}] } else { $p dec [expr {rand()}] } } } } after 1000 update_metrics } update_metrics # Let the application run proc bgerror {trouble} {puts stdout "bgerror: $trouble"} vwait forever
Let Prometheus know the metrics server is available at port 8043 by listing it as scrape target in the prometheus.yml config file
scrape_configs: # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config. - job_name: "prometheus" # metrics_path defaults to '/metrics' # scheme defaults to 'http'. static_configs: - targets: ["localhost:9090", "localhost:8043"]
Below you can see how the 2 gauges (with different labels) look in Prometheus: