Merge pull request 'Add Prometheus metrics endpoint' (#325) from Chewing_Bever/vieter:metrics into dev
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/lint Pipeline was successful Details
ci/woodpecker/push/arch Pipeline was successful Details
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/push/man Pipeline was successful Details
ci/woodpecker/push/docker Pipeline was successful Details
ci/woodpecker/push/deploy Pipeline was successful Details

Reviewed-on: #325
pull/339/head
Jef Roosens 2023-01-03 21:51:16 +01:00
commit 849bf54979
8 changed files with 69 additions and 6 deletions

View File

@ -8,15 +8,21 @@ branches:
depends_on: depends_on:
- build - build
skip_clone: true
pipeline: pipeline:
generate: install-modules:
image: *vlang_image image: *vlang_image
pull: true pull: true
commands: commands:
- curl -o vieter -L "https://s3.rustybever.be/vieter/commits/$CI_COMMIT_SHA/vieter-linux-amd64" - export VMODULES=$PWD/.vmodules
- chmod +x vieter - 'cd src && v install'
generate:
image: *vlang_image
commands:
# - curl -o vieter -L "https://s3.rustybever.be/vieter/commits/$CI_COMMIT_SHA/vieter-linux-amd64"
# - chmod +x vieter
- export VMODULES=$PWD/.vmodules
- make
- ./vieter man man - ./vieter man man
- cd man - cd man

View File

@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased](https://git.rustybever.be/vieter-v/vieter/src/branch/dev) ## [Unreleased](https://git.rustybever.be/vieter-v/vieter/src/branch/dev)
* Metrics endpoint for Prometheus integration
## [0.5.0](https://git.rustybever.be/vieter-v/vieter/src/tag/0.5.0) ## [0.5.0](https://git.rustybever.be/vieter-v/vieter/src/tag/0.5.0)
### Added ### Added

View File

@ -0,0 +1,19 @@
module server
import metrics
import web
// v1_metrics serves a Prometheus-compatible metrics endpoint.
['/api/v1/metrics'; get; markused]
fn (mut app App) v1_metrics() web.Result {
if !app.conf.collect_metrics {
return app.status(.not_found)
}
mut exporter := metrics.new_prometheus_exporter([0.01, 0.05, 0.1, 0.5, 1, 100])
exporter.load(app.collector)
// TODO stream to connection instead
body := exporter.export_to_string() or { return app.status(.internal_server_error) }
return app.body(.ok, 'text/plain', body)
}

View File

@ -15,6 +15,7 @@ pub:
base_image string = 'archlinux:base-devel' base_image string = 'archlinux:base-devel'
max_log_age int [empty_default] max_log_age int [empty_default]
log_removal_schedule string = '0 0' log_removal_schedule string = '0 0'
collect_metrics bool [empty_default]
} }
// cmd returns the cli submodule that handles starting the server // cmd returns the cli submodule that handles starting the server

View File

@ -8,6 +8,7 @@ import util
import db import db
import build { BuildJobQueue } import build { BuildJobQueue }
import cron.expression import cron.expression
import metrics
const ( const (
log_file_name = 'vieter.log' log_file_name = 'vieter.log'
@ -100,12 +101,19 @@ pub fn server(conf Config) ! {
util.exit_with_message(1, 'Failed to initialize database: $err.msg()') util.exit_with_message(1, 'Failed to initialize database: $err.msg()')
} }
collector := if conf.collect_metrics {
&metrics.MetricsCollector(metrics.new_default_collector())
} else {
&metrics.MetricsCollector(metrics.new_null_collector())
}
mut app := &App{ mut app := &App{
logger: logger logger: logger
api_key: conf.api_key api_key: conf.api_key
conf: conf conf: conf
repo: repo repo: repo
db: db db: db
collector: collector
job_queue: build.new_job_queue(global_ce, conf.base_image) job_queue: build.new_job_queue(global_ce, conf.base_image)
} }
app.init_job_queue() or { app.init_job_queue() or {

View File

@ -2,6 +2,7 @@ Module {
dependencies: [ dependencies: [
'https://git.rustybever.be/vieter-v/conf', 'https://git.rustybever.be/vieter-v/conf',
'https://git.rustybever.be/vieter-v/docker', 'https://git.rustybever.be/vieter-v/docker',
'https://git.rustybever.be/vieter-v/aur' 'https://git.rustybever.be/vieter-v/aur',
'https://git.rustybever.be/vieter-v/metrics'
] ]
} }

View File

@ -11,6 +11,7 @@ import net.urllib
import time import time
import json import json
import log import log
import metrics
// The Context struct represents the Context which hold the HTTP request and response. // The Context struct represents the Context which hold the HTTP request and response.
// It has fields for the query, form, files. // It has fields for the query, form, files.
@ -27,6 +28,8 @@ pub mut:
conn &net.TcpConn = unsafe { nil } conn &net.TcpConn = unsafe { nil }
// Gives access to a shared logger object // Gives access to a shared logger object
logger shared log.Log logger shared log.Log
// Used to collect metrics on the web server
collector &metrics.MetricsCollector
// time.ticks() from start of web connection handle. // time.ticks() from start of web connection handle.
// You can use it to determine how much time is spent on your request. // You can use it to determine how much time is spent on your request.
page_gen_start i64 page_gen_start i64
@ -145,6 +148,15 @@ pub fn (ctx &Context) is_authenticated() bool {
return false return false
} }
// body sends the given body as an HTTP response.
pub fn (mut ctx Context) body(status http.Status, content_type string, body string) Result {
ctx.status = status
ctx.content_type = content_type
ctx.send_response(body)
return Result{}
}
// json<T> HTTP_OK with json_s as payload with content-type `application/json` // json<T> HTTP_OK with json_s as payload with content-type `application/json`
pub fn (mut ctx Context) json<T>(status http.Status, j T) Result { pub fn (mut ctx Context) json<T>(status http.Status, j T) Result {
ctx.status = status ctx.status = status
@ -319,6 +331,18 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) {
app.logger.flush() app.logger.flush()
} }
// Record how long request took to process
labels := [
['method', app.req.method.str()]!,
['path', app.req.url]!,
['status', app.status.int().str()]!,
]
app.collector.counter_increment(name: 'http_requests_total', labels: labels)
app.collector.histogram_record(time.ticks() - app.page_gen_start,
name: 'http_requests_time_ms'
labels: labels
)
unsafe { unsafe {
free(app) free(app)
} }
@ -384,6 +408,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) {
static_mime_types: app.static_mime_types static_mime_types: app.static_mime_types
reader: reader reader: reader
logger: app.logger logger: app.logger
collector: app.collector
api_key: app.api_key api_key: app.api_key
} }

View File

@ -13,3 +13,4 @@ api_update_frequency = 2
image_rebuild_frequency = 1 image_rebuild_frequency = 1
max_concurrent_builds = 3 max_concurrent_builds = 3
max_log_age = 64 max_log_age = 64
collect_metrics = true