-
-
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.vscode
|
||||
*.db
|
||||
*.duckdb
|
||||
v
|
||||
12
datasource/Dockerfile
Normal file
12
datasource/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM golang:1.25.7-trixie AS build
|
||||
ENV CGO_ENABLED=1
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum .
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN go build -o main .
|
||||
|
||||
FROM ubuntu:24.04
|
||||
COPY --from=build /app/main /app/main
|
||||
WORKDIR /app
|
||||
CMD ["./main"]
|
||||
9
datasource/collector/collector.go
Normal file
9
datasource/collector/collector.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Collector interface {
|
||||
Collect(context.Context) error
|
||||
}
|
||||
77
datasource/collector/redis.go
Normal file
77
datasource/collector/redis.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"datasource/config"
|
||||
"datasource/store"
|
||||
"datasource/types"
|
||||
)
|
||||
|
||||
type RedisCollector struct {
|
||||
rdb *redis.Client
|
||||
store *store.Store
|
||||
}
|
||||
|
||||
func NewRedisCollector(cfg *config.RedisConfig, store *store.Store) *RedisCollector {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
})
|
||||
return &RedisCollector{rdb: rdb, store: store}
|
||||
}
|
||||
|
||||
func (c *RedisCollector) Close() error {
|
||||
return c.rdb.Close()
|
||||
}
|
||||
|
||||
func (c *RedisCollector) Collect(ctx context.Context) error {
|
||||
metric := new(types.Metric)
|
||||
|
||||
info, err := c.rdb.Info(ctx, "server", "memory").Result()
|
||||
|
||||
if err != nil {
|
||||
// todo (david): log but don't return the error as we still need to save the metric
|
||||
} else {
|
||||
metric.Up = 1
|
||||
|
||||
for _, line := range strings.Split(info, "\r\n") {
|
||||
kv := strings.Split(line, ":")
|
||||
if len(kv) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
k, v := kv[0], kv[1]
|
||||
|
||||
switch k {
|
||||
case "uptime_in_seconds":
|
||||
if uptimeSec, err := strconv.ParseInt(v, 10, 64); err != nil {
|
||||
return err
|
||||
} else {
|
||||
metric.UptimeSec = uptimeSec
|
||||
}
|
||||
case "used_memory":
|
||||
if memoryUsageBytes, err := strconv.ParseInt(v, 10, 64); err != nil {
|
||||
return err
|
||||
} else {
|
||||
metric.MemoryUsageBytes = memoryUsageBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metric.Service = "redis"
|
||||
metric.TimestampSec = time.Now().Unix()
|
||||
|
||||
if err := c.store.SaveMetric(ctx, metric); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
8
datasource/config.ini
Normal file
8
datasource/config.ini
Normal file
@@ -0,0 +1,8 @@
|
||||
[data_source]
|
||||
port=6060
|
||||
db_path=./data/metrics.duckdb
|
||||
|
||||
[redis]
|
||||
host=127.0.0.1
|
||||
port=6379
|
||||
; password=boingboing
|
||||
88
datasource/config/config.go
Normal file
88
datasource/config/config.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type DataSourceConfig struct {
|
||||
Port int
|
||||
DBPath string
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Password string
|
||||
}
|
||||
|
||||
func loadRequiredInt(sec *ini.Section, key string) (int, error) {
|
||||
val, err := sec.GetKey(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
res, err := val.Int()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func loadRequiredString(sec *ini.Section, key string) (string, error) {
|
||||
val, err := sec.GetKey(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res := strings.TrimSpace(val.String())
|
||||
if res == "" {
|
||||
return "", fmt.Errorf("%s cannot be empty", key)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func LoadDataSourceConfig(cfg *ini.File) (*DataSourceConfig, error) {
|
||||
sec, err := cfg.GetSection("data_source")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port, err := loadRequiredInt(sec, "port")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbPath, err := loadRequiredString(sec, "db_path")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DataSourceConfig{
|
||||
Port: port,
|
||||
DBPath: dbPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func LoadRedisConfig(cfg *ini.File) (*RedisConfig, error) {
|
||||
sec, err := cfg.GetSection("redis")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host, err := loadRequiredString(sec, "host")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port, err := loadRequiredInt(sec, "port")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RedisConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Password: strings.TrimSpace(sec.Key("password").String()),
|
||||
}, nil
|
||||
}
|
||||
71
datasource/controller/controller.go
Normal file
71
datasource/controller/controller.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"datasource/store"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
store *store.Store
|
||||
}
|
||||
|
||||
func NewController(store *store.Store) *Controller {
|
||||
return &Controller{store: store}
|
||||
}
|
||||
|
||||
func (c *Controller) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
service := query.Get("service")
|
||||
if service == "" {
|
||||
http.Error(w, "service cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
t0, err := strconv.ParseInt(query.Get("t0"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
t1, err := strconv.ParseInt(query.Get("t1"), 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if t0 > t1 {
|
||||
http.Error(w, "t0 cannot be larger than t1", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
metrics, err := c.store.GetMetrics(r.Context(), service, t0, t1)
|
||||
if err != nil {
|
||||
// todo (david): log internal server error
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := json.NewEncoder(&buf).Encode(metrics); err != nil {
|
||||
// todo (david): log internal server error
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if _, err := io.Copy(w, &buf); err != nil {
|
||||
// too late to return http.StatusInternalServerError
|
||||
// todo (david): log internal server error
|
||||
}
|
||||
}
|
||||
|
||||
// todo (david)
|
||||
// func (c *Controller) DeleteMetrics(w http.ResponseWriter, r *http.Request) {}
|
||||
36
datasource/go.mod
Normal file
36
datasource/go.mod
Normal file
@@ -0,0 +1,36 @@
|
||||
module datasource
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/duckdb/duckdb-go/v2 v2.5.5
|
||||
github.com/redis/go-redis/v9 v9.17.3
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apache/arrow-go/v18 v18.5.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/duckdb/duckdb-go-bindings v0.3.3 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.3.3 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/lib/darwin-arm64 v0.3.3 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/lib/linux-amd64 v0.3.3 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/lib/linux-arm64 v0.3.3 // indirect
|
||||
github.com/duckdb/duckdb-go-bindings/lib/windows-amd64 v0.3.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260116145544-c6413dc483f5 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
)
|
||||
96
datasource/go.sum
Normal file
96
datasource/go.sum
Normal file
@@ -0,0 +1,96 @@
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apache/arrow-go/v18 v18.5.1 h1:yaQ6zxMGgf9YCYw4/oaeOU3AULySDlAYDOcnr4LdHdI=
|
||||
github.com/apache/arrow-go/v18 v18.5.1/go.mod h1:OCCJsmdq8AsRm8FkBSSmYTwL/s4zHW9CqxeBxEytkNE=
|
||||
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
|
||||
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/duckdb/duckdb-go-bindings v0.3.3 h1:lXogtCY8hiGLQvTfK55HcgvaA3K2MrwKeZGqhIin35U=
|
||||
github.com/duckdb/duckdb-go-bindings v0.3.3/go.mod h1:zS7OpBP8zwVlP38OljRZOnqWYlNd4KLcVfMoA1JFzpk=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.3.3 h1:ue8BtIOSt+2Bt2fEfTAvBcQLxzBFhgfCcyzPtqQWTRA=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.3.3/go.mod h1:EnAvZh1kNJHp5yF+M1ZHNEvapnmt6anq1xXHVrAGqMo=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/darwin-arm64 v0.3.3 h1:2TrSeTgtwi3WIvub9ba0mny+AClSNo1w0Ghszc2B8lQ=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/darwin-arm64 v0.3.3/go.mod h1:IGLSeEcFhNeZF16aVjQCULD7TsFZKG5G7SyKJAXKp5c=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/linux-amd64 v0.3.3 h1:GN0cexhfE7uLb7qgDmsYG324wKF15nW+O7v5+NGalS4=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/linux-amd64 v0.3.3/go.mod h1:KAIynZ0GHCS7X5fRyuFnQMg/SZBPK/bS9OCOVojClxw=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/linux-arm64 v0.3.3 h1:bIJV+ct6yvMXjy+N3bfILFd0fkTK50AUhUTerkY40/8=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/linux-arm64 v0.3.3/go.mod h1:81SGOYoEUs8qaAfSk1wRfM5oobrIJ5KI7AzYhK6/bvQ=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/windows-amd64 v0.3.3 h1:SK2sunA/MPb2T3113iFzHv6DWeu+qrsw0DizTFrvM+Q=
|
||||
github.com/duckdb/duckdb-go-bindings/lib/windows-amd64 v0.3.3/go.mod h1:K25pJL26ARblGDeuAkrdblFvUen92+CwksLtPEHRqqQ=
|
||||
github.com/duckdb/duckdb-go/v2 v2.5.5 h1:TlK8ipnzoKW2aNrjGqRkFWLCDpJDxR/VwH8ezEcvVhw=
|
||||
github.com/duckdb/duckdb-go/v2 v2.5.5/go.mod h1:6uIbC3gz36NCEygECzboygOo/Z9TeVwox/puG+ohWV0=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
|
||||
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20260116145544-c6413dc483f5 h1:i0p03B68+xC1kD2QUO8JzDTPXCzhN56OLJ+IhHY8U3A=
|
||||
golang.org/x/telemetry v0.0.0-20260116145544-c6413dc483f5/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
|
||||
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
109
datasource/main.go
Normal file
109
datasource/main.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "github.com/duckdb/duckdb-go/v2"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"datasource/collector"
|
||||
"datasource/config"
|
||||
"datasource/controller"
|
||||
"datasource/store"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// todo (david): init logger
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
cfg, err := ini.Load("./config.ini")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
dataSourceConfig, err := config.LoadDataSourceConfig(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
store, err := store.NewStore(dataSourceConfig.DBPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
redisConfig, err := config.LoadRedisConfig(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
redisCollector := collector.NewRedisCollector(redisConfig, store)
|
||||
defer redisCollector.Close()
|
||||
|
||||
collectors := []collector.Collector{redisCollector}
|
||||
|
||||
for _, collector := range collectors {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := collector.Collect(ctx); err != nil {
|
||||
log.Printf("failed to collect: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
controller := controller.NewController(store)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /api/v1/metrics", controller.GetMetrics)
|
||||
// todo (david)
|
||||
// mux.HandleFunc("DELETE /api/v1/metrics", controller.DeleteMetrics)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf("0.0.0.0:%d", dataSourceConfig.Port),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("http.ListenAndServe: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
srv.Shutdown(context.Background())
|
||||
|
||||
srvShutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(srvShutdownCtx); err != nil {
|
||||
log.Fatalf("srv.Shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
124
datasource/store/duckdb.go
Normal file
124
datasource/store/duckdb.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"datasource/types"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewStore(dbPath string) (*Store, error) {
|
||||
db, err := sql.Open("duckdb", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// keep one connection ready for collector's next tick
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
// force sequential access to prevent DuckDB locking errors
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
migrationQuery := `
|
||||
CREATE TABLE IF NOT EXISTS metrics (
|
||||
service VARCHAR,
|
||||
timestamp_sec BIGINT,
|
||||
up INTEGER,
|
||||
uptime_sec BIGINT,
|
||||
memory_usage_bytes BIGINT
|
||||
)`
|
||||
|
||||
if _, err := db.Exec(migrationQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Store{db: db}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func (s *Store) SaveMetric(ctx context.Context, metric *types.Metric) error {
|
||||
query := `
|
||||
INSERT INTO metrics (
|
||||
service,
|
||||
timestamp_sec,
|
||||
up,
|
||||
uptime_sec,
|
||||
memory_usage_bytes
|
||||
) VALUES (?, ?, ?, ?, ?)`
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query,
|
||||
metric.Service,
|
||||
metric.TimestampSec,
|
||||
metric.Up,
|
||||
metric.UptimeSec,
|
||||
metric.MemoryUsageBytes,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetMetrics(ctx context.Context, service string, t0, t1 int64) ([]*types.Metric, error) {
|
||||
query := `
|
||||
SELECT
|
||||
timestamp_sec,
|
||||
up,
|
||||
uptime_sec,
|
||||
memory_usage_bytes
|
||||
FROM metrics
|
||||
WHERE
|
||||
service = ? AND
|
||||
timestamp_sec BETWEEN ? AND ?
|
||||
ORDER BY timestamp_sec`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, service, t0, t1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var metrics []*types.Metric
|
||||
|
||||
for rows.Next() {
|
||||
metric := &types.Metric{Service: service}
|
||||
if err := rows.Scan(&metric.TimestampSec, &metric.Up, &metric.UptimeSec, &metric.MemoryUsageBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics = append(metrics, metric)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetAllMetrics(ctx context.Context) ([]*types.Metric, error) {
|
||||
query := "SELECT service, timestamp_sec, up, uptime_sec, memory_usage_bytes FROM metrics"
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var metrics []*types.Metric
|
||||
|
||||
for rows.Next() {
|
||||
metric := new(types.Metric)
|
||||
if err := rows.Scan(&metric.Service, &metric.TimestampSec, &metric.Up, &metric.UptimeSec, &metric.MemoryUsageBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics = append(metrics, metric)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// todo (david)
|
||||
// func (s *Store) DeleteMetrics(ctx context.Context, t0, t1 int64) error {
|
||||
// return nil
|
||||
// }
|
||||
9
datasource/types/types.go
Normal file
9
datasource/types/types.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package types
|
||||
|
||||
type Metric struct {
|
||||
Service string `json:"service"`
|
||||
TimestampSec int64 `json:"timestamp_sec"` // unix epoch
|
||||
Up int `json:"up"` // 0: down, 1: healthy
|
||||
UptimeSec int64 `json:"uptime_sec"`
|
||||
MemoryUsageBytes int64 `json:"memory_usage_bytes"`
|
||||
}
|
||||
26
docker-compose.yaml
Normal file
26
docker-compose.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
datasource:
|
||||
build: ./datasource
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./datasource/config.ini:/app/config.ini:ro
|
||||
- ./v/datasource/data:/app/data
|
||||
restart: unless-stopped
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.3.2
|
||||
user: 1000:1000
|
||||
environment:
|
||||
- GF_PLUGINS_PREINSTALL_SYNC=yesoreyeram-infinity-datasource@3.7.0
|
||||
- GF_SECURITY_ADMIN_USER=pika
|
||||
- GF_SECURITY_ADMIN_PASSWORD=boingboing
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./v/grafana:/var/lib/grafana
|
||||
restart: unless-stopped
|
||||
|
||||
# redis:
|
||||
# image: redis:8.0.3-alpine3.21
|
||||
# ports:
|
||||
# - 127.0.0.1:6379:6379
|
||||
# restart: unless-stopped
|
||||
Reference in New Issue
Block a user