git init
This commit is contained in:
572
control-plane/backend/db.go
Normal file
572
control-plane/backend/db.go
Normal file
@@ -0,0 +1,572 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id text PRIMARY KEY,
|
||||
username text UNIQUE NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
role text NOT NULL CHECK (role IN ('viewer','operator','administrator')),
|
||||
disabled boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id_hash bytea PRIMARY KEY,
|
||||
user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
csrf_hash bytea NOT NULL,
|
||||
source_ip text NOT NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS sessions_expires_idx ON sessions(expires_at);
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id text PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
hostname text NOT NULL DEFAULT '',
|
||||
agent_endpoint text NOT NULL,
|
||||
credential_ref text NOT NULL,
|
||||
tags jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
status text NOT NULL DEFAULT 'unknown',
|
||||
last_seen timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS metric_samples (
|
||||
server_id text NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||
sampled_at timestamptz NOT NULL DEFAULT now(),
|
||||
cpu double precision NOT NULL,
|
||||
memory double precision NOT NULL,
|
||||
disk double precision NOT NULL,
|
||||
rx bigint NOT NULL,
|
||||
tx bigint NOT NULL,
|
||||
connections integer NOT NULL,
|
||||
bans integer NOT NULL,
|
||||
PRIMARY KEY(server_id, sampled_at)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS metric_samples_time_idx ON metric_samples(sampled_at);
|
||||
CREATE TABLE IF NOT EXISTS agent_log_entries (
|
||||
server_id text NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
||||
journal_cursor text NOT NULL,
|
||||
event_at timestamptz NOT NULL,
|
||||
unit text NOT NULL,
|
||||
priority smallint NOT NULL CHECK (priority BETWEEN 0 AND 7),
|
||||
message text NOT NULL,
|
||||
boot_id text,
|
||||
pid integer,
|
||||
ingested_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY(server_id, journal_cursor)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS agent_log_entries_time_idx ON agent_log_entries(event_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS agent_log_entries_server_time_idx ON agent_log_entries(server_id, event_at DESC);
|
||||
CREATE TABLE IF NOT EXISTS audit_events (
|
||||
id bigserial PRIMARY KEY,
|
||||
user_id text REFERENCES users(id) ON DELETE SET NULL,
|
||||
username text NOT NULL,
|
||||
server_id text REFERENCES servers(id) ON DELETE SET NULL,
|
||||
action text NOT NULL,
|
||||
target text NOT NULL,
|
||||
before_state jsonb,
|
||||
after_state jsonb,
|
||||
result text NOT NULL,
|
||||
source_ip text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS audit_events_time_idx ON audit_events(created_at DESC);
|
||||
`
|
||||
|
||||
type Store struct{ pool *pgxpool.Pool }
|
||||
|
||||
type Server struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname"`
|
||||
AgentEndpoint string `json:"agent_endpoint"`
|
||||
CredentialRef string `json:"-"`
|
||||
Tags json.RawMessage `json:"tags"`
|
||||
Status string `json:"status"`
|
||||
LastSeen *time.Time `json:"last_seen"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type AuditEvent struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
ServerID *string `json:"server_id"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Before json.RawMessage `json:"before"`
|
||||
After json.RawMessage `json:"after"`
|
||||
Result string `json:"result"`
|
||||
SourceIP string `json:"source_ip"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type MetricSample struct {
|
||||
SampledAt time.Time `json:"sampled_at"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory float64 `json:"memory"`
|
||||
Disk float64 `json:"disk"`
|
||||
RX int64 `json:"rx"`
|
||||
TX int64 `json:"tx"`
|
||||
Connections int `json:"connections"`
|
||||
Bans int `json:"bans"`
|
||||
}
|
||||
|
||||
type AgentLogEntry struct {
|
||||
ServerID string `json:"server_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Unit string `json:"unit"`
|
||||
Priority int `json:"priority"`
|
||||
Message string `json:"message"`
|
||||
BootID *string `json:"boot_id,omitempty"`
|
||||
PID *int `json:"pid,omitempty"`
|
||||
}
|
||||
|
||||
type MetricPoint struct {
|
||||
ServerID string
|
||||
ServerName string
|
||||
Timestamp time.Time
|
||||
Value float64
|
||||
}
|
||||
|
||||
type IntegrationLog struct {
|
||||
ServerID string
|
||||
ServerName string
|
||||
Timestamp time.Time
|
||||
Unit string
|
||||
Priority int
|
||||
Message string
|
||||
}
|
||||
|
||||
func OpenStore(ctx context.Context, databaseURL string) (*Store, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 30; attempt++ {
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err == nil {
|
||||
err = pool.Ping(ctx)
|
||||
}
|
||||
if err == nil {
|
||||
store := &Store{pool: pool}
|
||||
if _, err = pool.Exec(ctx, schema); err == nil {
|
||||
return store, nil
|
||||
}
|
||||
pool.Close()
|
||||
}
|
||||
lastErr = err
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("connect/migrate database: %w", lastErr)
|
||||
}
|
||||
|
||||
func (store *Store) Close() { store.pool.Close() }
|
||||
|
||||
func (store *Store) Bootstrap(ctx context.Context, config Config) error {
|
||||
var count int
|
||||
if err := store.pool.QueryRow(ctx, "SELECT count(*) FROM users").Scan(&count); err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if !validUsername(config.AdminUsername) || config.AdminPasswordFile == "" {
|
||||
return errors.New("first start requires INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD_FILE")
|
||||
}
|
||||
password, err := os.ReadFile(config.AdminPasswordFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read initial admin password: %w", err)
|
||||
}
|
||||
hash, err := HashPassword(strings.TrimSpace(string(password)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("initial admin password: %w", err)
|
||||
}
|
||||
if _, err = store.pool.Exec(ctx,
|
||||
"INSERT INTO users(id, username, password_hash, role) VALUES($1,$2,$3,$4)",
|
||||
newID(), config.AdminUsername, hash, RoleAdministrator); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("created initial administrator %q", config.AdminUsername)
|
||||
}
|
||||
if config.BootstrapEndpoint != "" {
|
||||
_, err := store.pool.Exec(ctx, `INSERT INTO servers(id,name,agent_endpoint,credential_ref)
|
||||
SELECT $1,$2,$3,$4 WHERE NOT EXISTS (SELECT 1 FROM servers)`,
|
||||
newID(), config.BootstrapServerName, config.BootstrapEndpoint, config.BootstrapCredential)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) LoginUser(ctx context.Context, username string) (User, string, bool, error) {
|
||||
var user User
|
||||
var hash string
|
||||
var disabled bool
|
||||
err := store.pool.QueryRow(ctx,
|
||||
"SELECT id,username,role,password_hash,disabled FROM users WHERE username=$1", username).
|
||||
Scan(&user.ID, &user.Username, &user.Role, &hash, &disabled)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return User{}, "", false, nil
|
||||
}
|
||||
return user, hash, disabled, err
|
||||
}
|
||||
|
||||
func (store *Store) CreateSession(ctx context.Context, userID, sourceIP string, ttl time.Duration) (string, string, error) {
|
||||
session, sessionHash, err := randomToken()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
csrf, csrfHash, err := randomToken()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
_, err = store.pool.Exec(ctx,
|
||||
"INSERT INTO sessions(id_hash,user_id,csrf_hash,source_ip,expires_at) VALUES($1,$2,$3,$4,$5)",
|
||||
sessionHash, userID, csrfHash, sourceIP, time.Now().Add(ttl))
|
||||
return session, csrf, err
|
||||
}
|
||||
|
||||
func (store *Store) SessionUser(ctx context.Context, token string) (User, []byte, error) {
|
||||
var user User
|
||||
var csrfHash []byte
|
||||
err := store.pool.QueryRow(ctx, `SELECT u.id,u.username,u.role,s.csrf_hash
|
||||
FROM sessions s JOIN users u ON u.id=s.user_id
|
||||
WHERE s.id_hash=$1 AND s.expires_at>now() AND NOT u.disabled`, tokenHash(token)).
|
||||
Scan(&user.ID, &user.Username, &user.Role, &csrfHash)
|
||||
return user, csrfHash, err
|
||||
}
|
||||
|
||||
func (store *Store) DeleteSession(ctx context.Context, token string) error {
|
||||
_, err := store.pool.Exec(ctx, "DELETE FROM sessions WHERE id_hash=$1", tokenHash(token))
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *Store) Servers(ctx context.Context) ([]Server, error) {
|
||||
rows, err := store.pool.Query(ctx, `SELECT id,name,hostname,agent_endpoint,credential_ref,tags,status,last_seen,created_at
|
||||
FROM servers ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
servers := []Server{}
|
||||
for rows.Next() {
|
||||
var server Server
|
||||
if err = rows.Scan(&server.ID, &server.Name, &server.Hostname, &server.AgentEndpoint,
|
||||
&server.CredentialRef, &server.Tags, &server.Status, &server.LastSeen, &server.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) Server(ctx context.Context, id string) (Server, error) {
|
||||
var server Server
|
||||
err := store.pool.QueryRow(ctx, `SELECT id,name,hostname,agent_endpoint,credential_ref,tags,status,last_seen,created_at
|
||||
FROM servers WHERE id=$1`, id).Scan(&server.ID, &server.Name, &server.Hostname, &server.AgentEndpoint,
|
||||
&server.CredentialRef, &server.Tags, &server.Status, &server.LastSeen, &server.CreatedAt)
|
||||
return server, err
|
||||
}
|
||||
|
||||
func (store *Store) CreateServer(ctx context.Context, server Server) error {
|
||||
_, err := store.pool.Exec(ctx, `INSERT INTO servers(id,name,hostname,agent_endpoint,credential_ref,tags)
|
||||
VALUES($1,$2,$3,$4,$5,$6)`, server.ID, server.Name, server.Hostname,
|
||||
server.AgentEndpoint, server.CredentialRef, server.Tags)
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *Store) Audit(ctx context.Context, user *User, serverID, action, target string,
|
||||
before, after any, result, sourceIP string) {
|
||||
var userID any
|
||||
username := "anonymous"
|
||||
if user != nil {
|
||||
userID, username = user.ID, user.Username
|
||||
}
|
||||
beforeJSON, _ := json.Marshal(before)
|
||||
afterJSON, _ := json.Marshal(after)
|
||||
if before == nil {
|
||||
beforeJSON = nil
|
||||
}
|
||||
if after == nil {
|
||||
afterJSON = nil
|
||||
}
|
||||
if _, err := store.pool.Exec(ctx, `INSERT INTO audit_events
|
||||
(user_id,username,server_id,action,target,before_state,after_state,result,source_ip)
|
||||
VALUES($1,$2,NULLIF($3,''),$4,$5,$6,$7,$8,$9)`, userID, username, serverID,
|
||||
action, target, beforeJSON, afterJSON, result, sourceIP); err != nil {
|
||||
log.Printf("write audit event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) AuditEvents(ctx context.Context, limit int) ([]AuditEvent, error) {
|
||||
rows, err := store.pool.Query(ctx, `SELECT id,username,server_id,action,target,
|
||||
COALESCE(before_state,'null'::jsonb),COALESCE(after_state,'null'::jsonb),result,source_ip,created_at
|
||||
FROM audit_events ORDER BY created_at DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
events := []AuditEvent{}
|
||||
for rows.Next() {
|
||||
var event AuditEvent
|
||||
if err = rows.Scan(&event.ID, &event.Username, &event.ServerID, &event.Action, &event.Target,
|
||||
&event.Before, &event.After, &event.Result, &event.SourceIP, &event.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
return events, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) Metrics(ctx context.Context, serverID string, limit int) ([]MetricSample, error) {
|
||||
rows, err := store.pool.Query(ctx, `SELECT sampled_at,cpu,memory,disk,rx,tx,connections,bans
|
||||
FROM metric_samples WHERE server_id=$1 ORDER BY sampled_at DESC LIMIT $2`, serverID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
samples := []MetricSample{}
|
||||
for rows.Next() {
|
||||
var sample MetricSample
|
||||
if err = rows.Scan(&sample.SampledAt, &sample.CPU, &sample.Memory, &sample.Disk,
|
||||
&sample.RX, &sample.TX, &sample.Connections, &sample.Bans); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
samples = append(samples, sample)
|
||||
}
|
||||
return samples, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) Logs(ctx context.Context, serverID string, limit int) ([]AgentLogEntry, error) {
|
||||
rows, err := store.pool.Query(ctx, `SELECT server_id,event_at,unit,priority,message,boot_id,pid
|
||||
FROM agent_log_entries WHERE server_id=$1 ORDER BY event_at DESC LIMIT $2`, serverID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
entries := []AgentLogEntry{}
|
||||
for rows.Next() {
|
||||
var entry AgentLogEntry
|
||||
if err = rows.Scan(&entry.ServerID, &entry.Timestamp, &entry.Unit, &entry.Priority,
|
||||
&entry.Message, &entry.BootID, &entry.PID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) MetricSeries(ctx context.Context) ([]map[string]string, error) {
|
||||
rows, err := store.pool.Query(ctx, "SELECT id,name FROM servers ORDER BY id LIMIT 1000")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
servers := [][2]string{}
|
||||
for rows.Next() {
|
||||
var server [2]string
|
||||
if err = rows.Scan(&server[0], &server[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, server)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := make([]string, 0, len(metricColumns))
|
||||
for name := range metricColumns {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
series := make([]map[string]string, 0, len(servers)*len(names))
|
||||
for _, server := range servers {
|
||||
for _, name := range names {
|
||||
series = append(series, map[string]string{
|
||||
"__name__": name, "server_id": server[0], "server_name": server[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
return series, nil
|
||||
}
|
||||
|
||||
func (store *Store) MetricLabelValues(ctx context.Context, label string) ([]string, error) {
|
||||
column := ""
|
||||
switch label {
|
||||
case "server_id":
|
||||
column = "id"
|
||||
case "server_name":
|
||||
column = "name"
|
||||
default:
|
||||
return nil, errors.New("unsupported metric label")
|
||||
}
|
||||
rows, err := store.pool.Query(ctx, "SELECT "+column+" FROM servers ORDER BY "+column+" LIMIT 10000")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
values := []string{}
|
||||
for rows.Next() {
|
||||
var value string
|
||||
if err = rows.Scan(&value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) InstantMetric(ctx context.Context, query metricQuery, at time.Time) ([]MetricPoint, error) {
|
||||
column, found := metricColumns[query.Name]
|
||||
if !found {
|
||||
return nil, errors.New("unsupported metric")
|
||||
}
|
||||
statement := fmt.Sprintf(`SELECT DISTINCT ON (m.server_id)
|
||||
m.server_id,s.name,m.sampled_at,m.%s::double precision
|
||||
FROM metric_samples m JOIN servers s ON s.id=m.server_id
|
||||
WHERE m.sampled_at <= $1 AND ($2='' OR s.id=$2) AND ($3='' OR s.name=$3)
|
||||
ORDER BY m.server_id,m.sampled_at DESC LIMIT 10000`, column)
|
||||
rows, err := store.pool.Query(ctx, statement, at, query.ServerID, query.ServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanMetricPoints(rows)
|
||||
}
|
||||
|
||||
func (store *Store) RangeMetric(ctx context.Context, query metricQuery, start, end time.Time, step int) ([]MetricPoint, error) {
|
||||
column, found := metricColumns[query.Name]
|
||||
if !found {
|
||||
return nil, errors.New("unsupported metric")
|
||||
}
|
||||
statement := fmt.Sprintf(`SELECT m.server_id,s.name,
|
||||
to_timestamp(floor(extract(epoch FROM m.sampled_at)/$3)*$3) AS bucket,
|
||||
avg(m.%s::double precision)
|
||||
FROM metric_samples m JOIN servers s ON s.id=m.server_id
|
||||
WHERE m.sampled_at BETWEEN $1 AND $2 AND ($4='' OR s.id=$4) AND ($5='' OR s.name=$5)
|
||||
GROUP BY m.server_id,s.name,bucket ORDER BY m.server_id,bucket LIMIT 100000`, column)
|
||||
rows, err := store.pool.Query(ctx, statement, start, end, step, query.ServerID, query.ServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanMetricPoints(rows)
|
||||
}
|
||||
|
||||
func scanMetricPoints(rows pgx.Rows) ([]MetricPoint, error) {
|
||||
points := []MetricPoint{}
|
||||
for rows.Next() {
|
||||
var point MetricPoint
|
||||
if err := rows.Scan(&point.ServerID, &point.ServerName, &point.Timestamp, &point.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
points = append(points, point)
|
||||
}
|
||||
return points, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) LogLabelValues(ctx context.Context, label string) ([]string, error) {
|
||||
column := ""
|
||||
switch label {
|
||||
case "server_id":
|
||||
column = "s.id"
|
||||
case "server_name":
|
||||
column = "s.name"
|
||||
case "unit":
|
||||
column = "l.unit"
|
||||
case "priority":
|
||||
column = "l.priority::text"
|
||||
default:
|
||||
return nil, errors.New("unsupported log label")
|
||||
}
|
||||
statement := `SELECT DISTINCT ` + column + ` AS value FROM agent_log_entries l
|
||||
JOIN servers s ON s.id=l.server_id ORDER BY value LIMIT 10000`
|
||||
rows, err := store.pool.Query(ctx, statement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
values := []string{}
|
||||
for rows.Next() {
|
||||
var value string
|
||||
if err = rows.Scan(&value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) LogSeries(ctx context.Context) ([]map[string]string, error) {
|
||||
rows, err := store.pool.Query(ctx, `SELECT DISTINCT s.id,s.name,l.unit,l.priority
|
||||
FROM agent_log_entries l JOIN servers s ON s.id=l.server_id
|
||||
ORDER BY s.id,l.unit,l.priority LIMIT 10000`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
series := []map[string]string{}
|
||||
for rows.Next() {
|
||||
var serverID, serverName, unit string
|
||||
var priority int
|
||||
if err = rows.Scan(&serverID, &serverName, &unit, &priority); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
series = append(series, map[string]string{
|
||||
"server_id": serverID, "server_name": serverName, "unit": unit,
|
||||
"priority": strconv.Itoa(priority),
|
||||
})
|
||||
}
|
||||
return series, rows.Err()
|
||||
}
|
||||
|
||||
func (store *Store) IntegrationLogs(ctx context.Context, start, end time.Time, limit int, direction string) ([]IntegrationLog, error) {
|
||||
order := "DESC"
|
||||
if direction == "forward" {
|
||||
order = "ASC"
|
||||
}
|
||||
statement := `SELECT l.server_id,s.name,l.event_at,l.unit,l.priority,l.message
|
||||
FROM agent_log_entries l JOIN servers s ON s.id=l.server_id
|
||||
WHERE l.event_at BETWEEN $1 AND $2 ORDER BY l.event_at ` + order + ` LIMIT $3`
|
||||
rows, err := store.pool.Query(ctx, statement, start, end, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
entries := []IntegrationLog{}
|
||||
for rows.Next() {
|
||||
var entry IntegrationLog
|
||||
if err = rows.Scan(&entry.ServerID, &entry.ServerName, &entry.Timestamp, &entry.Unit,
|
||||
&entry.Priority, &entry.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, rows.Err()
|
||||
}
|
||||
|
||||
func newID() string {
|
||||
var value [16]byte
|
||||
if _, err := rand.Read(value[:]); err != nil {
|
||||
panic("system random source unavailable")
|
||||
}
|
||||
value[6] = (value[6] & 0x0f) | 0x40
|
||||
value[8] = (value[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
value[0:4], value[4:6], value[6:8], value[8:10], value[10:16])
|
||||
}
|
||||
Reference in New Issue
Block a user