Add config validation and dashboard
Add YAML config loading with validation, env overrides for secrets, and a web dashboard exposing health, stats, and recent events.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
module sources
|
||||
|
||||
pub fn transform_cron(raw string, source CronSource) ?Event {
|
||||
return Event{
|
||||
source: 'cron'
|
||||
name: source.name
|
||||
topic: source.topic
|
||||
priority: source.priority_map['default'] or { 0 }
|
||||
tags: source.tags.clone()
|
||||
message: raw
|
||||
raw: raw
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
module sources
|
||||
|
||||
import x.json2 as json
|
||||
|
||||
pub fn transform_docker_event(raw string, source DockerSource, host string) ?Event {
|
||||
doc := json.decode[json.Any](raw, json.DecoderOptions{}) or { return none }
|
||||
obj := doc.as_map()
|
||||
|
||||
event_type := get_string(obj, 'status')
|
||||
actor := obj['Actor'] or { json.Any(map[string]json.Any{}) }.as_map()
|
||||
attrs := actor['Attributes'] or { json.Any(map[string]json.Any{}) }.as_map()
|
||||
|
||||
container_name := get_string(attrs, 'name')
|
||||
image := get_string(attrs, 'image')
|
||||
exit_code := get_string(attrs, 'exitCode')
|
||||
|
||||
priority := source.priority_map[event_type] or { 0 }
|
||||
|
||||
mut tags := source.tags.clone()
|
||||
if event_type == 'oom' && 'skull' !in tags {
|
||||
tags << 'skull'
|
||||
}
|
||||
|
||||
if source.template != '' {
|
||||
vars := {
|
||||
'container_name': container_name
|
||||
'status': event_type
|
||||
'image': image
|
||||
'exit_code': exit_code
|
||||
'host': host
|
||||
}
|
||||
return Event{
|
||||
source: 'docker'
|
||||
name: source.name
|
||||
topic: source.topic
|
||||
priority: priority
|
||||
tags: tags
|
||||
message: render_source_template(source.template, vars)
|
||||
raw: raw
|
||||
}
|
||||
}
|
||||
|
||||
emoji := match event_type {
|
||||
'die' { '💀' }
|
||||
'oom' { '☠️' }
|
||||
'health_status' { '💓' }
|
||||
else { '🐳' }
|
||||
}
|
||||
|
||||
mut message := '${emoji} ${container_name} → ${event_type}'
|
||||
if image != '' {
|
||||
message += '\nImage: ${image}'
|
||||
}
|
||||
if exit_code != '' {
|
||||
message += '\nExit code: ${exit_code}'
|
||||
}
|
||||
message += '\nHost: ${host}'
|
||||
|
||||
return Event{
|
||||
source: 'docker'
|
||||
name: source.name
|
||||
topic: source.topic
|
||||
priority: priority
|
||||
tags: tags
|
||||
message: message
|
||||
raw: raw
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
module sources
|
||||
|
||||
pub struct Event {
|
||||
pub mut:
|
||||
source string
|
||||
name string
|
||||
topic string
|
||||
priority int
|
||||
tags []string
|
||||
message string
|
||||
raw string
|
||||
}
|
||||
|
||||
pub fn (e Event) clone() Event {
|
||||
return Event{
|
||||
source: e.source
|
||||
name: e.name
|
||||
topic: e.topic
|
||||
priority: e.priority
|
||||
tags: e.tags.clone()
|
||||
message: e.message
|
||||
raw: e.raw
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GiteaSource {
|
||||
pub mut:
|
||||
name string @[json: 'name']
|
||||
webhook_path string @[json: 'webhook_path']
|
||||
topic string @[json: 'topic']
|
||||
template string @[json: 'template']
|
||||
priority_map map[string]int @[json: 'priority_map']
|
||||
tags []string @[json: 'tags']
|
||||
repo string @[json: 'repo']
|
||||
}
|
||||
|
||||
pub struct UptimeKumaState {
|
||||
pub mut:
|
||||
priority int @[json: 'priority']
|
||||
tags []string @[json: 'tags']
|
||||
}
|
||||
|
||||
pub struct UptimeKumaSource {
|
||||
pub mut:
|
||||
name string @[json: 'name']
|
||||
webhook_path string @[json: 'webhook_path']
|
||||
topic string @[json: 'topic']
|
||||
template string @[json: 'template']
|
||||
priority_map map[string]int @[json: 'priority_map']
|
||||
tags []string @[json: 'tags']
|
||||
state_map map[string]UptimeKumaState @[json: 'state_map']
|
||||
}
|
||||
|
||||
pub struct DockerSource {
|
||||
pub mut:
|
||||
name string @[json: 'name']
|
||||
topic string @[json: 'topic']
|
||||
template string @[json: 'template']
|
||||
priority_map map[string]int @[json: 'priority_map']
|
||||
tags []string @[json: 'tags']
|
||||
hosts []string @[json: 'hosts']
|
||||
events []string @[json: 'events']
|
||||
}
|
||||
|
||||
pub struct HttpCheck {
|
||||
pub mut:
|
||||
url string @[json: 'url']
|
||||
topic string @[json: 'topic']
|
||||
priority int @[json: 'priority']
|
||||
expect_status int @[json: 'expect_status']
|
||||
timeout int @[json: 'timeout']
|
||||
template string @[json: 'template']
|
||||
}
|
||||
|
||||
pub struct HttpPollSource {
|
||||
pub mut:
|
||||
name string @[json: 'name']
|
||||
interval int @[json: 'interval']
|
||||
checks []HttpCheck @[json: 'checks']
|
||||
}
|
||||
|
||||
pub struct CronSource {
|
||||
pub mut:
|
||||
name string @[json: 'name']
|
||||
webhook_path string @[json: 'webhook_path']
|
||||
topic string @[json: 'topic']
|
||||
template string @[json: 'template']
|
||||
priority_map map[string]int @[json: 'priority_map']
|
||||
tags []string @[json: 'tags']
|
||||
}
|
||||
|
||||
pub struct GenericSource {
|
||||
pub mut:
|
||||
name string @[json: 'name']
|
||||
webhook_path string @[json: 'webhook_path']
|
||||
topic string @[json: 'topic']
|
||||
template string @[json: 'template']
|
||||
priority_map map[string]int @[json: 'priority_map']
|
||||
tags []string @[json: 'tags']
|
||||
}
|
||||
|
||||
// render_source_template applies variable substitution to a template string.
|
||||
// Placeholders are {varname}. Shared by all source transformers in this module.
|
||||
pub fn render_source_template(template string, vars map[string]string) string {
|
||||
mut result := template.clone()
|
||||
for key, value in vars {
|
||||
placeholder := '{${key}}'
|
||||
result = result.replace(placeholder, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
module sources
|
||||
|
||||
pub fn transform_generic(raw string, source GenericSource) ?Event {
|
||||
return Event{
|
||||
source: 'generic'
|
||||
name: source.name
|
||||
topic: source.topic
|
||||
priority: source.priority_map['default'] or { 0 }
|
||||
tags: source.tags.clone()
|
||||
message: raw
|
||||
raw: raw
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
module sources
|
||||
|
||||
import x.json2 as json
|
||||
|
||||
pub fn transform_gitea(raw string, source GiteaSource) ?Event {
|
||||
doc := json.decode[json.Any](raw, json.DecoderOptions{}) or { return none }
|
||||
obj := doc.as_map()
|
||||
|
||||
event_type := detect_gitea_event(obj)
|
||||
priority := source.priority_map[event_type] or { 0 }
|
||||
|
||||
repo := get_string(obj, 'repository.full_name')
|
||||
mut user := get_string(obj, 'pusher.username')
|
||||
action := get_string(obj, 'action')
|
||||
mut title := get_string(obj, 'commits.0.message')
|
||||
mut sha := get_string(obj, 'commits.0.id')
|
||||
|
||||
if event_type == 'pull_request' {
|
||||
pr := obj['pull_request'] or { json.Any(map[string]json.Any{}) }.as_map()
|
||||
title = get_string(pr, 'title')
|
||||
sha = get_string(pr, 'merge_commit_sha')
|
||||
if user == '' {
|
||||
user = get_string(pr, 'user.login')
|
||||
}
|
||||
} else if event_type == 'issue' {
|
||||
issue := obj['issue'] or { json.Any(map[string]json.Any{}) }.as_map()
|
||||
title = get_string(issue, 'title')
|
||||
sha = ''
|
||||
if user == '' {
|
||||
user = get_string(issue, 'user.login')
|
||||
}
|
||||
}
|
||||
|
||||
template := source.template
|
||||
mut message := ''
|
||||
if template != '' {
|
||||
vars := {
|
||||
'repo': repo
|
||||
'user': user
|
||||
'action': action
|
||||
'title': title
|
||||
'sha': sha
|
||||
}
|
||||
message = render_source_template(template, vars)
|
||||
} else {
|
||||
mut parts := []string{}
|
||||
if repo != '' {
|
||||
parts << '[${repo}]'
|
||||
}
|
||||
if user != '' {
|
||||
parts << '${user}'
|
||||
}
|
||||
if action != '' {
|
||||
parts << '${action}:'
|
||||
}
|
||||
if title != '' {
|
||||
parts << '"${title}"'
|
||||
}
|
||||
if sha != '' {
|
||||
short := if sha.len >= 7 { sha[..7] } else { sha }
|
||||
parts << '(${short})'
|
||||
}
|
||||
emoji := match event_type {
|
||||
'pull_request' { '🔀' }
|
||||
'issue' { '📝' }
|
||||
'release' { '🚀' }
|
||||
else { '🔨' }
|
||||
}
|
||||
|
||||
message = '${emoji} ' + parts.join(' ')
|
||||
}
|
||||
|
||||
return Event{
|
||||
source: 'gitea'
|
||||
name: source.name
|
||||
topic: source.topic
|
||||
priority: priority
|
||||
tags: source.tags.clone()
|
||||
message: message
|
||||
raw: raw
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_gitea_event(obj map[string]json.Any) string {
|
||||
if 'commits' in obj {
|
||||
return 'push'
|
||||
}
|
||||
if 'pull_request' in obj {
|
||||
return 'pull_request'
|
||||
}
|
||||
if 'issue' in obj {
|
||||
return 'issue'
|
||||
}
|
||||
if 'release' in obj {
|
||||
return 'release'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
fn get_string(obj map[string]json.Any, path string) string {
|
||||
parts := path.split('.')
|
||||
mut current := obj.clone()
|
||||
for i, part in parts {
|
||||
if i == parts.len - 1 {
|
||||
val := current[part] or { return '' }
|
||||
return val.str()
|
||||
}
|
||||
next := current[part] or { return '' }
|
||||
current = next.as_map().clone()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
module sources
|
||||
|
||||
pub fn transform_http_down(check HttpCheck, err string) Event {
|
||||
mut message := ''
|
||||
if check.template != '' {
|
||||
vars := {
|
||||
'url': check.url
|
||||
'error': err
|
||||
'status': ''
|
||||
}
|
||||
message = render_source_template(check.template, vars)
|
||||
} else {
|
||||
message = '❌ ${check.url} → ${err}'
|
||||
}
|
||||
return Event{
|
||||
source: 'http_poll'
|
||||
name: check.url
|
||||
topic: check.topic
|
||||
priority: check.priority
|
||||
tags: ['x']
|
||||
message: message
|
||||
raw: err
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_http_up(check HttpCheck) Event {
|
||||
message := '✅ ${check.url} is UP'
|
||||
return Event{
|
||||
source: 'http_poll'
|
||||
name: check.url
|
||||
topic: check.topic
|
||||
priority: 1
|
||||
tags: ['white_check_mark']
|
||||
message: message
|
||||
raw: ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
module sources
|
||||
|
||||
import x.json2 as json
|
||||
|
||||
pub fn transform_uptime_kuma(raw string, source UptimeKumaSource) ?Event {
|
||||
doc := json.decode[json.Any](raw, json.DecoderOptions{}) or { return none }
|
||||
obj := doc.as_map()
|
||||
|
||||
monitor := get_string(obj, 'monitor.name')
|
||||
status := get_string(obj, 'heartbeat.status')
|
||||
msg := get_string(obj, 'heartbeat.msg')
|
||||
ping := get_string(obj, 'heartbeat.ping')
|
||||
time_str := get_string(obj, 'heartbeat.time')
|
||||
|
||||
state_cfg := source.state_map[status] or { UptimeKumaState{} }
|
||||
priority := state_cfg.priority
|
||||
mut tags := state_cfg.tags.clone()
|
||||
|
||||
if source.tags.len > 0 && tags.len == 0 {
|
||||
tags << source.tags
|
||||
}
|
||||
|
||||
emoji := if status == 'up' { '✅' } else { '🚨' }
|
||||
mut message := ''
|
||||
if source.template != '' {
|
||||
vars := {
|
||||
'monitor': monitor
|
||||
'status': status
|
||||
'msg': msg
|
||||
'ping': ping
|
||||
'time': time_str
|
||||
}
|
||||
message = render_source_template(source.template, vars)
|
||||
} else {
|
||||
message = '${emoji} ${monitor} is ${status.to_upper()}'
|
||||
if msg != '' {
|
||||
message += ' — ${msg}'
|
||||
}
|
||||
if time_str != '' {
|
||||
message += ' — ${time_str}'
|
||||
}
|
||||
}
|
||||
|
||||
return Event{
|
||||
source: 'uptime_kuma'
|
||||
name: source.name
|
||||
topic: source.topic
|
||||
priority: priority
|
||||
tags: tags
|
||||
message: message
|
||||
raw: raw
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user