573 lines
18 KiB
Go
573 lines
18 KiB
Go
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])
|
|
}
|