Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 267c279f3d | |||
| eb5459abf3 | |||
| 8262b1442f | |||
| d4bf8d9e16 |
@@ -3,6 +3,7 @@ package chdb
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
@@ -105,3 +106,127 @@ func (d *ClickHouse) Logscores(ctx context.Context, serverID, monitorID int, sin
|
|||||||
|
|
||||||
return rv, nil
|
return rv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogscoresTimeRange queries log scores within a specific time range for Grafana integration
|
||||||
|
func (d *ClickHouse) LogscoresTimeRange(ctx context.Context, serverID, monitorID int, from, to time.Time, limit int) ([]ntpdb.LogScore, error) {
|
||||||
|
log := logger.Setup()
|
||||||
|
ctx, span := tracing.Tracer().Start(ctx, "CH LogscoresTimeRange")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
args := []interface{}{serverID, from, to}
|
||||||
|
|
||||||
|
query := `select id,monitor_id,server_id,ts,
|
||||||
|
toFloat64(score),toFloat64(step),offset,
|
||||||
|
rtt,leap,warning,error
|
||||||
|
from log_scores
|
||||||
|
where
|
||||||
|
server_id = ?
|
||||||
|
and ts >= ?
|
||||||
|
and ts <= ?`
|
||||||
|
|
||||||
|
if monitorID > 0 {
|
||||||
|
query += " and monitor_id = ?"
|
||||||
|
args = append(args, monitorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always order by timestamp ASC for Grafana convention
|
||||||
|
query += " order by ts ASC"
|
||||||
|
|
||||||
|
// Apply limit to prevent memory issues
|
||||||
|
if limit > 0 {
|
||||||
|
query += " limit ?"
|
||||||
|
args = append(args, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.DebugContext(ctx, "clickhouse time range query",
|
||||||
|
"query", query,
|
||||||
|
"args", args,
|
||||||
|
"server_id", serverID,
|
||||||
|
"monitor_id", monitorID,
|
||||||
|
"from", from.Format(time.RFC3339),
|
||||||
|
"to", to.Format(time.RFC3339),
|
||||||
|
"limit", limit,
|
||||||
|
"full_sql_with_params", func() string {
|
||||||
|
// Build a readable SQL query with parameters substituted for debugging
|
||||||
|
sqlDebug := query
|
||||||
|
paramIndex := 0
|
||||||
|
for strings.Contains(sqlDebug, "?") && paramIndex < len(args) {
|
||||||
|
var replacement string
|
||||||
|
switch v := args[paramIndex].(type) {
|
||||||
|
case int:
|
||||||
|
replacement = fmt.Sprintf("%d", v)
|
||||||
|
case time.Time:
|
||||||
|
replacement = fmt.Sprintf("'%s'", v.Format("2006-01-02 15:04:05"))
|
||||||
|
default:
|
||||||
|
replacement = fmt.Sprintf("'%v'", v)
|
||||||
|
}
|
||||||
|
sqlDebug = strings.Replace(sqlDebug, "?", replacement, 1)
|
||||||
|
paramIndex++
|
||||||
|
}
|
||||||
|
return sqlDebug
|
||||||
|
}(),
|
||||||
|
)
|
||||||
|
|
||||||
|
rows, err := d.Scores.Query(
|
||||||
|
clickhouse.Context(
|
||||||
|
ctx, clickhouse.WithSpan(span.SpanContext()),
|
||||||
|
),
|
||||||
|
query, args...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorContext(ctx, "time range query error", "err", err)
|
||||||
|
return nil, fmt.Errorf("database error")
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := []ntpdb.LogScore{}
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
row := ntpdb.LogScore{}
|
||||||
|
var leap uint8
|
||||||
|
|
||||||
|
if err := rows.Scan(
|
||||||
|
&row.ID,
|
||||||
|
&row.MonitorID,
|
||||||
|
&row.ServerID,
|
||||||
|
&row.Ts,
|
||||||
|
&row.Score,
|
||||||
|
&row.Step,
|
||||||
|
&row.Offset,
|
||||||
|
&row.Rtt,
|
||||||
|
&leap,
|
||||||
|
&row.Attributes.Warning,
|
||||||
|
&row.Attributes.Error,
|
||||||
|
); err != nil {
|
||||||
|
log.Error("could not parse row", "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
row.Attributes.Leap = int8(leap)
|
||||||
|
rv = append(rv, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfoContext(ctx, "time range query results",
|
||||||
|
"rows_returned", len(rv),
|
||||||
|
"server_id", serverID,
|
||||||
|
"monitor_id", monitorID,
|
||||||
|
"time_range", fmt.Sprintf("%s to %s", from.Format(time.RFC3339), to.Format(time.RFC3339)),
|
||||||
|
"limit", limit,
|
||||||
|
"sample_rows", func() []map[string]interface{} {
|
||||||
|
samples := make([]map[string]interface{}, 0, 3)
|
||||||
|
for i, row := range rv {
|
||||||
|
if i >= 3 { break }
|
||||||
|
samples = append(samples, map[string]interface{}{
|
||||||
|
"id": row.ID,
|
||||||
|
"monitor_id": row.MonitorID,
|
||||||
|
"ts": row.Ts.Format(time.RFC3339),
|
||||||
|
"score": row.Score,
|
||||||
|
"rtt_valid": row.Rtt.Valid,
|
||||||
|
"offset_valid": row.Offset.Valid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return samples
|
||||||
|
}(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|||||||
40
go.mod
40
go.mod
@@ -4,6 +4,8 @@ go 1.24
|
|||||||
|
|
||||||
// replace github.com/samber/slog-echo => github.com/abh/slog-echo v0.0.0-20231024051244-af740639893e
|
// replace github.com/samber/slog-echo => github.com/abh/slog-echo v0.0.0-20231024051244-af740639893e
|
||||||
|
|
||||||
|
replace go.opentelemetry.io/otel/exporters/prometheus v0.59.1 => go.opentelemetry.io/otel/exporters/prometheus v0.59.0
|
||||||
|
|
||||||
tool (
|
tool (
|
||||||
github.com/hexdigest/gowrap/cmd/gowrap
|
github.com/hexdigest/gowrap/cmd/gowrap
|
||||||
github.com/sqlc-dev/sqlc/cmd/sqlc
|
github.com/sqlc-dev/sqlc/cmd/sqlc
|
||||||
@@ -12,7 +14,7 @@ tool (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.2
|
dario.cat/mergo v1.0.2
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.37.2
|
github.com/ClickHouse/clickhouse-go/v2 v2.40.1
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
github.com/labstack/echo-contrib v0.17.4
|
github.com/labstack/echo-contrib v0.17.4
|
||||||
@@ -20,27 +22,27 @@ require (
|
|||||||
github.com/samber/slog-echo v1.16.1
|
github.com/samber/slog-echo v1.16.1
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
go.ntppool.org/api v0.3.4
|
go.ntppool.org/api v0.3.4
|
||||||
go.ntppool.org/common v0.4.3
|
go.ntppool.org/common v0.5.1
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.62.0
|
go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.62.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.62.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.62.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0
|
||||||
go.opentelemetry.io/otel v1.37.0
|
go.opentelemetry.io/otel v1.37.0
|
||||||
go.opentelemetry.io/otel/trace v1.37.0
|
go.opentelemetry.io/otel/trace v1.37.0
|
||||||
golang.org/x/sync v0.15.0
|
golang.org/x/sync v0.16.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.23.0 // indirect
|
cel.dev/expr v0.24.0 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/ClickHouse/ch-go v0.66.1 // indirect
|
github.com/ClickHouse/ch-go v0.67.0 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cubicdaiya/gonp v1.0.4 // indirect
|
github.com/cubicdaiya/gonp v1.0.4 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
@@ -81,7 +83,7 @@ require (
|
|||||||
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect
|
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.65.0 // indirect
|
github.com/prometheus/common v0.65.0 // indirect
|
||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
@@ -94,7 +96,7 @@ require (
|
|||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/spf13/cast v1.4.1 // indirect
|
github.com/spf13/cast v1.4.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.7 // indirect
|
||||||
github.com/sqlc-dev/sqlc v1.29.0 // indirect
|
github.com/sqlc-dev/sqlc v1.29.0 // indirect
|
||||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||||
@@ -113,7 +115,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.59.0 // indirect
|
go.opentelemetry.io/otel/exporters/prometheus v0.59.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 // indirect
|
||||||
@@ -122,21 +124,21 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect
|
go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
golang.org/x/mod v0.25.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.27.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
golang.org/x/tools v0.33.0 // indirect
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
|
||||||
google.golang.org/grpc v1.73.0 // indirect
|
google.golang.org/grpc v1.74.2 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
modernc.org/libc v1.62.1 // indirect
|
modernc.org/libc v1.62.1 // indirect
|
||||||
|
|||||||
71
go.sum
71
go.sum
@@ -1,14 +1,14 @@
|
|||||||
cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss=
|
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||||
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/ClickHouse/ch-go v0.66.1 h1:LQHFslfVYZsISOY0dnOYOXGkOUvpv376CCm8g7W74A4=
|
github.com/ClickHouse/ch-go v0.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc=
|
||||||
github.com/ClickHouse/ch-go v0.66.1/go.mod h1:NEYcg3aOFv2EmTJfo4m2WF7sHB/YFbLUuIWv9iq76xY=
|
github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.37.2 h1:wRLNKoynvHQEN4znnVHNLaYnrqVc9sGJmGYg+GGCfto=
|
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.37.2/go.mod h1:pH2zrBGp5Y438DMwAxXMm1neSXPPjSI7tD4MURVULw8=
|
github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||||
@@ -22,8 +22,8 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
|
|||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
@@ -155,8 +155,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||||
@@ -190,8 +190,9 @@ github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
|
|||||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||||
|
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs=
|
github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs=
|
||||||
github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0=
|
github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0=
|
||||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||||
@@ -227,8 +228,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
|||||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||||
go.ntppool.org/api v0.3.4 h1:KeRyFhIRkjJwZif7hkpqEDEBmukyYGiOi2Fd6j3UzQ0=
|
go.ntppool.org/api v0.3.4 h1:KeRyFhIRkjJwZif7hkpqEDEBmukyYGiOi2Fd6j3UzQ0=
|
||||||
go.ntppool.org/api v0.3.4/go.mod h1:LFLAwnrc/JyjzKnjgf8tCOJhps6oFIjuledS3PCx7xc=
|
go.ntppool.org/api v0.3.4/go.mod h1:LFLAwnrc/JyjzKnjgf8tCOJhps6oFIjuledS3PCx7xc=
|
||||||
go.ntppool.org/common v0.4.3 h1:IByoorl2RMNf6EBTORl3MOZB5mTSnjYBQxn44U3v4HA=
|
go.ntppool.org/common v0.5.1 h1:MSkfNGLBosqmbnYJxX/fCAE9kaAgUWeOZ4zQNWWrs6o=
|
||||||
go.ntppool.org/common v0.4.3/go.mod h1:8ILmR3KxpUSNofcw9EBG42HNf81Z9iu9Fg1Cj0f/WP0=
|
go.ntppool.org/common v0.5.1/go.mod h1:e5ohROK9LdZZTI1neNiSlmgmWC23F779qzLvSi4JzyI=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
|
||||||
@@ -283,8 +284,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
@@ -305,29 +306,29 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -335,15 +336,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -353,18 +354,18 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package ntpdb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
@@ -82,14 +81,14 @@ func (_d QuerierTxWithTracing) Commit(ctx context.Context) (err error) {
|
|||||||
return _d.QuerierTx.Commit(ctx)
|
return _d.QuerierTx.Commit(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMonitorByName implements QuerierTx
|
// GetMonitorByNameAndIPVersion implements QuerierTx
|
||||||
func (_d QuerierTxWithTracing) GetMonitorByName(ctx context.Context, tlsName sql.NullString) (m1 Monitor, err error) {
|
func (_d QuerierTxWithTracing) GetMonitorByNameAndIPVersion(ctx context.Context, arg GetMonitorByNameAndIPVersionParams) (m1 Monitor, err error) {
|
||||||
ctx, _span := otel.Tracer(_d._instance).Start(ctx, "QuerierTx.GetMonitorByName")
|
ctx, _span := otel.Tracer(_d._instance).Start(ctx, "QuerierTx.GetMonitorByNameAndIPVersion")
|
||||||
defer func() {
|
defer func() {
|
||||||
if _d._spanDecorator != nil {
|
if _d._spanDecorator != nil {
|
||||||
_d._spanDecorator(_span, map[string]interface{}{
|
_d._spanDecorator(_span, map[string]interface{}{
|
||||||
"ctx": ctx,
|
"ctx": ctx,
|
||||||
"tlsName": tlsName}, map[string]interface{}{
|
"arg": arg}, map[string]interface{}{
|
||||||
"m1": m1,
|
"m1": m1,
|
||||||
"err": err})
|
"err": err})
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -103,7 +102,7 @@ func (_d QuerierTxWithTracing) GetMonitorByName(ctx context.Context, tlsName sql
|
|||||||
|
|
||||||
_span.End()
|
_span.End()
|
||||||
}()
|
}()
|
||||||
return _d.QuerierTx.GetMonitorByName(ctx, tlsName)
|
return _d.QuerierTx.GetMonitorByNameAndIPVersion(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMonitorsByID implements QuerierTx
|
// GetMonitorsByID implements QuerierTx
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ package ntpdb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Querier interface {
|
type Querier interface {
|
||||||
GetMonitorByName(ctx context.Context, tlsName sql.NullString) (Monitor, error)
|
GetMonitorByNameAndIPVersion(ctx context.Context, arg GetMonitorByNameAndIPVersionParams) (Monitor, error)
|
||||||
GetMonitorsByID(ctx context.Context, monitorids []uint32) ([]Monitor, error)
|
GetMonitorsByID(ctx context.Context, monitorids []uint32) ([]Monitor, error)
|
||||||
GetServerByID(ctx context.Context, id uint32) (Server, error)
|
GetServerByID(ctx context.Context, id uint32) (Server, error)
|
||||||
GetServerByIP(ctx context.Context, ip string) (Server, error)
|
GetServerByIP(ctx context.Context, ip string) (Server, error)
|
||||||
|
|||||||
@@ -12,16 +12,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const getMonitorByName = `-- name: GetMonitorByName :one
|
const getMonitorByNameAndIPVersion = `-- name: GetMonitorByNameAndIPVersion :one
|
||||||
select id, id_token, type, user_id, account_id, hostname, location, ip, ip_version, tls_name, api_key, status, config, client_version, last_seen, last_submit, created_on, deleted_on, is_current from monitors
|
select id, id_token, type, user_id, account_id, hostname, location, ip, ip_version, tls_name, api_key, status, config, client_version, last_seen, last_submit, created_on, deleted_on, is_current from monitors
|
||||||
where
|
where
|
||||||
tls_name like ?
|
tls_name like ? AND
|
||||||
|
ip_version = ? AND
|
||||||
|
is_current = 1 AND
|
||||||
|
status != 'deleted'
|
||||||
order by id
|
order by id
|
||||||
limit 1
|
limit 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetMonitorByName(ctx context.Context, tlsName sql.NullString) (Monitor, error) {
|
type GetMonitorByNameAndIPVersionParams struct {
|
||||||
row := q.db.QueryRowContext(ctx, getMonitorByName, tlsName)
|
TlsName sql.NullString `db:"tls_name" json:"tls_name"`
|
||||||
|
IpVersion NullMonitorsIpVersion `db:"ip_version" json:"ip_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetMonitorByNameAndIPVersion(ctx context.Context, arg GetMonitorByNameAndIPVersionParams) (Monitor, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getMonitorByNameAndIPVersion, arg.TlsName, arg.IpVersion)
|
||||||
var i Monitor
|
var i Monitor
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
|
|||||||
389
plans/grafana-time-range-api.md
Normal file
389
plans/grafana-time-range-api.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# DETAILED IMPLEMENTATION PLAN: Grafana Time Range API with Future Downsampling Support
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Implement a new Grafana-compatible API endpoint `/api/v2/server/scores/{server}/{mode}` that returns time series data in Grafana format with time range support and future downsampling capabilities.
|
||||||
|
|
||||||
|
## API Specification
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
- **URL**: `/api/v2/server/scores/{server}/{mode}`
|
||||||
|
- **Method**: GET
|
||||||
|
- **Path Parameters**:
|
||||||
|
- `server`: Server IP address or ID (same validation as existing API)
|
||||||
|
- `mode`: Only `json` supported initially
|
||||||
|
|
||||||
|
### Query Parameters (following Grafana conventions)
|
||||||
|
- `from`: Unix timestamp in seconds (required)
|
||||||
|
- `to`: Unix timestamp in seconds (required)
|
||||||
|
- `maxDataPoints`: Integer, default 50000, max 50000 (for future downsampling)
|
||||||
|
- `monitor`: Monitor ID, name prefix, or "*" for all (optional, same as existing)
|
||||||
|
- `interval`: Future downsampling interval like "1m", "5m", "1h" (optional, not implemented initially)
|
||||||
|
|
||||||
|
### Response Format
|
||||||
|
Grafana table format JSON array (more efficient than separate series):
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"target": "monitor{name=zakim1-yfhw4a}",
|
||||||
|
"tags": {
|
||||||
|
"monitor_id": "126",
|
||||||
|
"monitor_name": "zakim1-yfhw4a",
|
||||||
|
"type": "monitor",
|
||||||
|
"status": "active"
|
||||||
|
},
|
||||||
|
"columns": [
|
||||||
|
{"text": "time", "type": "time"},
|
||||||
|
{"text": "score", "type": "number"},
|
||||||
|
{"text": "rtt", "type": "number", "unit": "ms"},
|
||||||
|
{"text": "offset", "type": "number", "unit": "s"}
|
||||||
|
],
|
||||||
|
"values": [
|
||||||
|
[1753431667000, 20.0, 18.865, -0.000267],
|
||||||
|
[1753431419000, 20.0, 18.96, -0.000390],
|
||||||
|
[1753431151000, 20.0, 18.073, -0.000768],
|
||||||
|
[1753430063000, 20.0, 18.209, null]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Server Routing (`server/server.go`)
|
||||||
|
Add new route after existing scores routes:
|
||||||
|
```go
|
||||||
|
e.GET("/api/v2/server/scores/:server/:mode", srv.scoresTimeRange)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Initially attempted `:server.:mode` pattern, but Echo router cannot properly parse IP addresses with dots using this pattern. Changed to `:server/:mode` to match existing API pattern and ensure compatibility with IP addresses like `23.155.40.38`.
|
||||||
|
|
||||||
|
## Key Implementation Clarifications
|
||||||
|
|
||||||
|
### Monitor Filtering Behavior
|
||||||
|
- **monitor=\***: Return ALL monitors (no monitor count limit)
|
||||||
|
- **50k datapoint limit**: Applied in database query (LIMIT clause)
|
||||||
|
- Return whatever data we get from database to user (no post-processing truncation)
|
||||||
|
|
||||||
|
### Null Value Handling Strategy
|
||||||
|
- **Score**: Always include (should never be null)
|
||||||
|
- **RTT**: Skip datapoints where RTT is null
|
||||||
|
- **Offset**: Skip datapoints where offset is null
|
||||||
|
|
||||||
|
### Time Range Validation Rules
|
||||||
|
- **Zero duration**: Return 400 Bad Request
|
||||||
|
- **Future timestamps**: Allow for now
|
||||||
|
- **Minimum range**: 1 second
|
||||||
|
- **Maximum range**: 90 days
|
||||||
|
|
||||||
|
### 2. New Handler Function (`server/grafana.go`)
|
||||||
|
|
||||||
|
#### Function Signature
|
||||||
|
```go
|
||||||
|
func (srv *Server) scoresTimeRange(c echo.Context) error
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameter Parsing & Validation
|
||||||
|
```go
|
||||||
|
// Extend existing historyParameters struct for time range support
|
||||||
|
type timeRangeParams struct {
|
||||||
|
historyParameters // embed existing struct
|
||||||
|
from time.Time
|
||||||
|
to time.Time
|
||||||
|
maxDataPoints int
|
||||||
|
interval string // for future downsampling
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) parseTimeRangeParams(ctx context.Context, c echo.Context) (timeRangeParams, error) {
|
||||||
|
// Start with existing parameter parsing logic
|
||||||
|
baseParams, err := srv.getHistoryParameters(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return timeRangeParams{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate from/to second timestamps
|
||||||
|
// Validate time range (max 90 days, min 1 second)
|
||||||
|
// Parse maxDataPoints (default 50000, max 50000)
|
||||||
|
// Return extended parameters
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Structure
|
||||||
|
```go
|
||||||
|
type ColumnDef struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Unit string `json:"unit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrafanaTableSeries struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Tags map[string]string `json:"tags"`
|
||||||
|
Columns []ColumnDef `json:"columns"`
|
||||||
|
Values [][]interface{} `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrafanaTimeSeriesResponse []GrafanaTableSeries
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cache Control
|
||||||
|
```go
|
||||||
|
// Reuse existing setHistoryCacheControl function for consistency
|
||||||
|
// Logic based on data recency and entry count:
|
||||||
|
// - Empty or >8h old data: "s-maxage=260,max-age=360"
|
||||||
|
// - Single entry: "s-maxage=60,max-age=35"
|
||||||
|
// - Multiple entries: "s-maxage=90,max-age=120"
|
||||||
|
setHistoryCacheControl(c, history)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ClickHouse Data Access (`chdb/logscores.go`)
|
||||||
|
|
||||||
|
#### New Method
|
||||||
|
```go
|
||||||
|
func (d *ClickHouse) LogscoresTimeRange(ctx context.Context, serverID, monitorID int, from, to time.Time, limit int) ([]ntpdb.LogScore, error) {
|
||||||
|
// Build query with time range WHERE clause
|
||||||
|
// Always order by ts ASC (Grafana convention)
|
||||||
|
// Apply limit to prevent memory issues
|
||||||
|
// Use same row scanning logic as existing Logscores method
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Query Structure
|
||||||
|
```sql
|
||||||
|
SELECT id, monitor_id, server_id, ts,
|
||||||
|
toFloat64(score), toFloat64(step), offset,
|
||||||
|
rtt, leap, warning, error
|
||||||
|
FROM log_scores
|
||||||
|
WHERE server_id = ?
|
||||||
|
AND ts >= ?
|
||||||
|
AND ts <= ?
|
||||||
|
[AND monitor_id = ?] -- if specific monitor requested
|
||||||
|
ORDER BY ts ASC
|
||||||
|
LIMIT ?
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Data Transformation Logic (`server/grafana.go`)
|
||||||
|
|
||||||
|
#### Core Transformation Function
|
||||||
|
```go
|
||||||
|
func transformToGrafanaTableFormat(history *logscores.LogScoreHistory, monitors []ntpdb.Monitor) GrafanaTimeSeriesResponse {
|
||||||
|
// Group data by monitor_id (one series per monitor)
|
||||||
|
// Create table format with columns: time, score, rtt, offset
|
||||||
|
// Convert timestamps to milliseconds
|
||||||
|
// Build proper target names and tags
|
||||||
|
// Handle null values appropriately in table values
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Grouping Strategy
|
||||||
|
1. **Group by Monitor**: One table series per monitor
|
||||||
|
2. **Table Columns**: time, score, rtt, offset (all metrics in one table)
|
||||||
|
3. **Target Naming**: `monitor{name={sanitized_monitor_name}}`
|
||||||
|
4. **Tag Structure**: Include monitor metadata (no metric type needed)
|
||||||
|
5. **Monitor Status**: Query real monitor data using `q.GetServerScores()` like existing API
|
||||||
|
6. **Series Ordering**: No guaranteed order (standard Grafana behavior)
|
||||||
|
7. **Efficiency**: More efficient than separate series - less JSON overhead
|
||||||
|
|
||||||
|
#### Timestamp Conversion
|
||||||
|
```go
|
||||||
|
timestampMs := logScore.Ts.Unix() * 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Error Handling
|
||||||
|
|
||||||
|
#### Validation Errors (400 Bad Request)
|
||||||
|
- Invalid timestamp format
|
||||||
|
- from >= to (including zero duration)
|
||||||
|
- Time range too large (> 90 days)
|
||||||
|
- Time range too small (< 1 second minimum)
|
||||||
|
- maxDataPoints > 50000
|
||||||
|
- Invalid mode (not "json")
|
||||||
|
|
||||||
|
#### Not Found Errors (404)
|
||||||
|
- Server not found
|
||||||
|
- Monitor not found
|
||||||
|
- Server deleted
|
||||||
|
|
||||||
|
#### Server Errors (500)
|
||||||
|
- ClickHouse connection issues
|
||||||
|
- Database query errors
|
||||||
|
|
||||||
|
### 6. Future Downsampling Design
|
||||||
|
|
||||||
|
#### API Extension Points
|
||||||
|
- `interval` parameter parsing ready
|
||||||
|
- `maxDataPoints` limit already enforced
|
||||||
|
- Response format supports downsampled data seamlessly
|
||||||
|
|
||||||
|
#### Downsampling Algorithm (Future Implementation)
|
||||||
|
```go
|
||||||
|
// When datapoints > maxDataPoints:
|
||||||
|
// 1. Calculate downsample interval: (to - from) / maxDataPoints
|
||||||
|
// 2. Group data into time buckets
|
||||||
|
// 3. Aggregate per bucket: avg for score/rtt, last for offset
|
||||||
|
// 4. Return aggregated datapoints
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Parameter parsing and validation
|
||||||
|
- Data transformation logic
|
||||||
|
- Error handling scenarios
|
||||||
|
- Timestamp conversion accuracy
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- End-to-end API requests
|
||||||
|
- ClickHouse query execution
|
||||||
|
- Multiple monitor scenarios
|
||||||
|
- Large time range handling
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
- Grafana integration testing
|
||||||
|
- Performance with various time ranges
|
||||||
|
- Cache behavior validation
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
- 50k datapoint limit applied in database query (LIMIT clause) (covers ~few weeks of data)
|
||||||
|
- ClickHouse-only for better range query performance
|
||||||
|
- Proper indexing on (server_id, ts) assumed
|
||||||
|
- Table format more efficient than separate time series (less JSON overhead)
|
||||||
|
|
||||||
|
### Future Optimizations (Critical for Production)
|
||||||
|
- **Downsampling for large ranges**: Essential for 90-day queries with reasonable performance
|
||||||
|
- Query optimization based on range size
|
||||||
|
- Potential parallel monitor queries
|
||||||
|
- Adaptive sampling rates based on time range duration
|
||||||
|
|
||||||
|
## Documentation Updates
|
||||||
|
|
||||||
|
### API.md Addition
|
||||||
|
```markdown
|
||||||
|
### 7. Server Scores Time Range (v2)
|
||||||
|
|
||||||
|
**GET** `/api/v2/server/scores/{server}/{mode}`
|
||||||
|
|
||||||
|
Grafana-compatible time series endpoint for NTP server scoring data.
|
||||||
|
|
||||||
|
#### Path Parameters
|
||||||
|
- `server`: Server IP address or ID
|
||||||
|
- `mode`: Response format (`json` only)
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
- `from`: Start time as Unix timestamp in seconds (required)
|
||||||
|
- `to`: End time as Unix timestamp in seconds (required)
|
||||||
|
- `maxDataPoints`: Maximum data points to return (default: 50000, max: 50000)
|
||||||
|
- `monitor`: Monitor filter (ID, name prefix, or "*" for all)
|
||||||
|
|
||||||
|
#### Response Format
|
||||||
|
Grafana table format array with one series per monitor containing all metrics as columns.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Research Findings
|
||||||
|
|
||||||
|
### Grafana Error Format Requirements
|
||||||
|
- **HTTP Status Codes**: Standard 400/404/500 work fine
|
||||||
|
- **Response Body**: JSON preferred with `Content-Type: application/json`
|
||||||
|
- **Structure**: Simple `{"error": "message", "status": code}` is sufficient
|
||||||
|
- **Compatibility**: Existing Echo error patterns are Grafana-compatible
|
||||||
|
|
||||||
|
### Data Volume Considerations
|
||||||
|
- **50k Datapoint Limit**: Only covers ~few weeks of data, not sufficient for 90-day ranges
|
||||||
|
- **Downsampling Critical**: Required for production use with 90-day time ranges
|
||||||
|
- **Current Approach**: Acceptable for MVP, downsampling essential for full utility
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Phase 0: Grafana Table Format Validation ✅ **COMPLETED**
|
||||||
|
- [x] Add test endpoint `/api/v2/test/grafana-table` returning sample table format
|
||||||
|
- [x] Implement Grafana table format response structures in `server/grafana.go`
|
||||||
|
- [x] Add structured logging and OpenTelemetry tracing to test endpoint
|
||||||
|
- [x] Verify endpoint compiles and serves correct JSON format
|
||||||
|
- [x] Test endpoint response format and headers (CORS, Content-Type, Cache-Control)
|
||||||
|
- [ ] Test with actual Grafana instance to validate table format compatibility
|
||||||
|
- [ ] Confirm time series panels render table format correctly
|
||||||
|
- [ ] Validate column types and units display properly
|
||||||
|
|
||||||
|
#### Phase 0 Implementation Details
|
||||||
|
**Files Created/Modified:**
|
||||||
|
- `server/grafana.go`: New file containing Grafana table format structures and test endpoint
|
||||||
|
- `server/server.go`: Added route `e.GET("/api/v2/test/grafana-table", srv.testGrafanaTable)`
|
||||||
|
|
||||||
|
**Test Endpoint Features:**
|
||||||
|
- **URL**: `http://localhost:8030/api/v2/test/grafana-table`
|
||||||
|
- **Response Format**: Grafana table format with realistic NTP Pool data
|
||||||
|
- **Sample Data**: Two monitor series (zakim1-yfhw4a, nj2-mon01) with time-based values
|
||||||
|
- **Columns**: time, score, rtt (ms), offset (s) with proper units
|
||||||
|
- **Null Handling**: Demonstrates null offset values
|
||||||
|
- **Headers**: CORS, JSON content-type, cache control
|
||||||
|
- **Observability**: Structured logging with context, OpenTelemetry tracing
|
||||||
|
|
||||||
|
**Recommended Grafana Data Source**: JSON API plugin (`marcusolsson-json-datasource`) - ideal for REST APIs returning table format JSON
|
||||||
|
|
||||||
|
### Phase 1: Core Implementation ✅ **COMPLETED**
|
||||||
|
- [x] Add route in server.go (fixed routing pattern from `:server.:mode` to `:server/:mode`)
|
||||||
|
- [x] Implement parseTimeRangeParams function for parameter validation
|
||||||
|
- [x] Add LogscoresTimeRange method to ClickHouse with time range filtering
|
||||||
|
- [x] Implement transformToGrafanaTableFormat function with monitor grouping
|
||||||
|
- [x] Add scoresTimeRange handler with full error handling
|
||||||
|
- [x] Error handling and validation (reuse existing Echo patterns)
|
||||||
|
- [x] Cache control headers (reuse setHistoryCacheControl)
|
||||||
|
|
||||||
|
#### Phase 1 Implementation Details
|
||||||
|
**Key Components Built:**
|
||||||
|
- **Route Pattern**: `/api/v2/server/scores/:server/:mode` (matches existing API consistency)
|
||||||
|
- **Parameter Validation**: Full validation of `from`/`to` timestamps, `maxDataPoints`, time ranges
|
||||||
|
- **ClickHouse Integration**: `LogscoresTimeRange()` with time-based WHERE clauses and ASC ordering
|
||||||
|
- **Data Transformation**: Grafana table format with monitor grouping and null value handling
|
||||||
|
- **Complete Handler**: `scoresTimeRange()` with server validation, error handling, caching, and CORS
|
||||||
|
|
||||||
|
**Routing Fix**: Changed from `:server.:mode` to `:server/:mode` to resolve Echo router issue with IP addresses containing dots (e.g., `23.155.40.38`).
|
||||||
|
|
||||||
|
**Files Created/Modified in Phase 1:**
|
||||||
|
- `server/grafana.go`: Complete implementation with all structures and functions
|
||||||
|
- `timeRangeParams` struct and `parseTimeRangeParams()` function
|
||||||
|
- `transformToGrafanaTableFormat()` function with monitor grouping
|
||||||
|
- `scoresTimeRange()` handler with full error handling
|
||||||
|
- `sanitizeMonitorName()` utility function
|
||||||
|
- `server/server.go`: Added route `e.GET("/api/v2/server/scores/:server/:mode", srv.scoresTimeRange)`
|
||||||
|
- `chdb/logscores.go`: Added `LogscoresTimeRange()` method for time-based queries
|
||||||
|
|
||||||
|
**Production Testing Results** (July 25, 2025):
|
||||||
|
- ✅ **Real Data Verification**: Successfully tested with server `102.64.112.164` over 12-hour time range
|
||||||
|
- ✅ **Multiple Monitor Support**: Returns data for multiple monitors (`defra1-210hw9t`, `recentmedian`)
|
||||||
|
- ✅ **Data Quality Validation**:
|
||||||
|
- RTT conversion (microseconds → milliseconds): ✅ Working
|
||||||
|
- Timestamp conversion (seconds → milliseconds): ✅ Working
|
||||||
|
- Null value handling: ✅ Working (recentmedian has null RTT/offset as expected)
|
||||||
|
- Monitor grouping: ✅ Working (one series per monitor)
|
||||||
|
- ✅ **API Parameter Changes**: Successfully changed from milliseconds to seconds for user-friendliness
|
||||||
|
- ✅ **Volume Testing**: Handles 100+ data points per monitor efficiently
|
||||||
|
- ✅ **Error Handling**: All validation working (400 for invalid params, 404 for missing servers)
|
||||||
|
- ✅ **Performance**: Sub-second response times for 12-hour ranges
|
||||||
|
|
||||||
|
**Sample Working Request:**
|
||||||
|
```bash
|
||||||
|
curl 'http://localhost:8030/api/v2/server/scores/102.64.112.164/json?from=1753457764&to=1753500964&monitor=*'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Testing & Polish
|
||||||
|
- [ ] Unit tests for all functions
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Manual Grafana testing with real data
|
||||||
|
- [ ] Performance testing with large ranges (up to 50k points)
|
||||||
|
- [ ] API documentation updates
|
||||||
|
|
||||||
|
### Phase 3: Future Enhancement Ready
|
||||||
|
- [ ] Interval parameter parsing (no-op initially)
|
||||||
|
- [ ] Downsampling framework hooks (critical for 90-day ranges)
|
||||||
|
- [ ] Monitoring and metrics for new endpoint
|
||||||
|
|
||||||
|
This design provides a solid foundation for immediate Grafana integration while being fully prepared for future downsampling capabilities without breaking changes.
|
||||||
|
|
||||||
|
## Critical Notes for Production
|
||||||
|
|
||||||
|
- **Downsampling Required**: 50k datapoint limit means 90-day ranges will hit limits quickly
|
||||||
|
- **Table Format Validation**: Phase 0 testing ensures Grafana compatibility before full implementation
|
||||||
|
- **Error Handling**: Existing Echo patterns are sufficient for Grafana requirements
|
||||||
|
- **Scalability**: Current design handles weeks of data well, downsampling needed for months
|
||||||
@@ -47,10 +47,13 @@ select * from servers
|
|||||||
where
|
where
|
||||||
ip = sqlc.arg(ip);
|
ip = sqlc.arg(ip);
|
||||||
|
|
||||||
-- name: GetMonitorByName :one
|
-- name: GetMonitorByNameAndIPVersion :one
|
||||||
select * from monitors
|
select * from monitors
|
||||||
where
|
where
|
||||||
tls_name like sqlc.arg('tls_name')
|
tls_name like sqlc.arg('tls_name') AND
|
||||||
|
ip_version = sqlc.arg('ip_version') AND
|
||||||
|
is_current = 1 AND
|
||||||
|
status != 'deleted'
|
||||||
order by id
|
order by id
|
||||||
limit 1;
|
limit 1;
|
||||||
|
|
||||||
|
|||||||
28
schema.sql
28
schema.sql
@@ -3,7 +3,7 @@
|
|||||||
--
|
--
|
||||||
-- Host: ntpdb-haproxy.ntpdb.svc.cluster.local Database: askntp
|
-- Host: ntpdb-haproxy.ntpdb.svc.cluster.local Database: askntp
|
||||||
-- ------------------------------------------------------
|
-- ------------------------------------------------------
|
||||||
-- Server version 8.0.40-31
|
-- Server version 8.0.42-33
|
||||||
|
|
||||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
@@ -383,6 +383,28 @@ SET character_set_client = utf8mb4;
|
|||||||
1 AS `last_submit` */;
|
1 AS `last_submit` */;
|
||||||
SET character_set_client = @saved_cs_client;
|
SET character_set_client = @saved_cs_client;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `oidc_public_keys`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `oidc_public_keys`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `oidc_public_keys` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`kid` varchar(255) NOT NULL,
|
||||||
|
`public_key` text NOT NULL,
|
||||||
|
`algorithm` varchar(20) NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL,
|
||||||
|
`expires_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`active` tinyint(1) NOT NULL DEFAULT '1',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `kid` (`kid`),
|
||||||
|
KEY `idx_kid` (`kid`),
|
||||||
|
KEY `idx_active_expires` (`active`,`expires_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `schema_revision`
|
-- Table structure for table `schema_revision`
|
||||||
--
|
--
|
||||||
@@ -469,7 +491,7 @@ CREATE TABLE `server_scores` (
|
|||||||
`score_ts` datetime DEFAULT NULL,
|
`score_ts` datetime DEFAULT NULL,
|
||||||
`score_raw` double NOT NULL DEFAULT '0',
|
`score_raw` double NOT NULL DEFAULT '0',
|
||||||
`stratum` tinyint unsigned DEFAULT NULL,
|
`stratum` tinyint unsigned DEFAULT NULL,
|
||||||
`status` enum('new','candidate','testing','active') NOT NULL DEFAULT 'new',
|
`status` enum('candidate','testing','active') NOT NULL DEFAULT 'candidate',
|
||||||
`queue_ts` datetime DEFAULT NULL,
|
`queue_ts` datetime DEFAULT NULL,
|
||||||
`created_on` datetime NOT NULL,
|
`created_on` datetime NOT NULL,
|
||||||
`modified_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`modified_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
@@ -876,4 +898,4 @@ CREATE TABLE `zones` (
|
|||||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
|
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
|
||||||
|
|
||||||
-- Dump completed on 2025-06-27 9:46:22
|
-- Dump completed on 2025-08-03 0:43:29
|
||||||
|
|||||||
539
server/grafana.go
Normal file
539
server/grafana.go
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"go.ntppool.org/common/logger"
|
||||||
|
"go.ntppool.org/common/tracing"
|
||||||
|
"go.ntppool.org/data-api/logscores"
|
||||||
|
"go.ntppool.org/data-api/ntpdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColumnDef represents a Grafana table column definition
|
||||||
|
type ColumnDef struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Unit string `json:"unit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GrafanaTableSeries represents a single table series in Grafana format
|
||||||
|
type GrafanaTableSeries struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Tags map[string]string `json:"tags"`
|
||||||
|
Columns []ColumnDef `json:"columns"`
|
||||||
|
Values [][]interface{} `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GrafanaTimeSeriesResponse represents the complete Grafana table response
|
||||||
|
type GrafanaTimeSeriesResponse []GrafanaTableSeries
|
||||||
|
|
||||||
|
// timeRangeParams extends historyParameters with time range support
|
||||||
|
type timeRangeParams struct {
|
||||||
|
historyParameters // embed existing struct
|
||||||
|
from time.Time
|
||||||
|
to time.Time
|
||||||
|
maxDataPoints int
|
||||||
|
interval string // for future downsampling
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTimeRangeParams parses and validates time range parameters
|
||||||
|
func (srv *Server) parseTimeRangeParams(ctx context.Context, c echo.Context, server ntpdb.Server) (timeRangeParams, error) {
|
||||||
|
log := logger.FromContext(ctx)
|
||||||
|
|
||||||
|
// Start with existing parameter parsing logic
|
||||||
|
baseParams, err := srv.getHistoryParameters(ctx, c, server)
|
||||||
|
if err != nil {
|
||||||
|
return timeRangeParams{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
trParams := timeRangeParams{
|
||||||
|
historyParameters: baseParams,
|
||||||
|
maxDataPoints: 50000, // default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse from timestamp (required)
|
||||||
|
fromParam := c.QueryParam("from")
|
||||||
|
if fromParam == "" {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "from parameter is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
fromSec, err := strconv.ParseInt(fromParam, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "invalid from timestamp format")
|
||||||
|
}
|
||||||
|
trParams.from = time.Unix(fromSec, 0)
|
||||||
|
|
||||||
|
// Parse to timestamp (required)
|
||||||
|
toParam := c.QueryParam("to")
|
||||||
|
if toParam == "" {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "to parameter is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
toSec, err := strconv.ParseInt(toParam, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "invalid to timestamp format")
|
||||||
|
}
|
||||||
|
trParams.to = time.Unix(toSec, 0)
|
||||||
|
|
||||||
|
// Validate time range
|
||||||
|
if trParams.from.Equal(trParams.to) || trParams.from.After(trParams.to) {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "from must be before to")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check minimum range (1 second)
|
||||||
|
if trParams.to.Sub(trParams.from) < time.Second {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "time range must be at least 1 second")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check maximum range (90 days)
|
||||||
|
if trParams.to.Sub(trParams.from) > 90*24*time.Hour {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "time range cannot exceed 90 days")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse maxDataPoints (optional)
|
||||||
|
if maxDataPointsParam := c.QueryParam("maxDataPoints"); maxDataPointsParam != "" {
|
||||||
|
maxDP, err := strconv.Atoi(maxDataPointsParam)
|
||||||
|
if err != nil {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "invalid maxDataPoints format")
|
||||||
|
}
|
||||||
|
if maxDP > 50000 {
|
||||||
|
return timeRangeParams{}, echo.NewHTTPError(http.StatusBadRequest, "maxDataPoints cannot exceed 50000")
|
||||||
|
}
|
||||||
|
if maxDP > 0 {
|
||||||
|
trParams.maxDataPoints = maxDP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse interval (optional, for future downsampling)
|
||||||
|
trParams.interval = c.QueryParam("interval")
|
||||||
|
|
||||||
|
log.DebugContext(ctx, "parsed time range params",
|
||||||
|
"from", trParams.from,
|
||||||
|
"to", trParams.to,
|
||||||
|
"maxDataPoints", trParams.maxDataPoints,
|
||||||
|
"interval", trParams.interval,
|
||||||
|
"monitor", trParams.monitorID,
|
||||||
|
)
|
||||||
|
|
||||||
|
return trParams, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeMonitorName sanitizes monitor names for Grafana target format
|
||||||
|
func sanitizeMonitorName(name string) string {
|
||||||
|
// Replace problematic characters for Grafana target format
|
||||||
|
result := strings.ReplaceAll(name, " ", "_")
|
||||||
|
result = strings.ReplaceAll(result, ".", "-")
|
||||||
|
result = strings.ReplaceAll(result, "/", "-")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// transformToGrafanaTableFormat converts LogScoreHistory to Grafana table format
|
||||||
|
func transformToGrafanaTableFormat(history *logscores.LogScoreHistory, monitors []ntpdb.Monitor) GrafanaTimeSeriesResponse {
|
||||||
|
// Group data by monitor_id (one series per monitor)
|
||||||
|
monitorData := make(map[int][]ntpdb.LogScore)
|
||||||
|
monitorInfo := make(map[int]ntpdb.Monitor)
|
||||||
|
|
||||||
|
// Group log scores by monitor ID
|
||||||
|
skippedInvalidMonitors := 0
|
||||||
|
for _, ls := range history.LogScores {
|
||||||
|
if !ls.MonitorID.Valid {
|
||||||
|
skippedInvalidMonitors++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
monitorID := int(ls.MonitorID.Int32)
|
||||||
|
monitorData[monitorID] = append(monitorData[monitorID], ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging for transformation
|
||||||
|
logger.Setup().Info("transformation grouping debug",
|
||||||
|
"total_log_scores", len(history.LogScores),
|
||||||
|
"skipped_invalid_monitors", skippedInvalidMonitors,
|
||||||
|
"grouped_monitor_ids", func() []int {
|
||||||
|
keys := make([]int, 0, len(monitorData))
|
||||||
|
for k := range monitorData {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}(),
|
||||||
|
"monitor_data_counts", func() map[int]int {
|
||||||
|
counts := make(map[int]int)
|
||||||
|
for k, v := range monitorData {
|
||||||
|
counts[k] = len(v)
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Index monitors by ID for quick lookup
|
||||||
|
for _, monitor := range monitors {
|
||||||
|
monitorInfo[int(monitor.ID)] = monitor
|
||||||
|
}
|
||||||
|
|
||||||
|
var response GrafanaTimeSeriesResponse
|
||||||
|
|
||||||
|
// Create one table series per monitor
|
||||||
|
logger.Setup().Info("creating grafana series",
|
||||||
|
"monitor_data_entries", len(monitorData),
|
||||||
|
)
|
||||||
|
|
||||||
|
for monitorID, logScores := range monitorData {
|
||||||
|
if len(logScores) == 0 {
|
||||||
|
logger.Setup().Info("skipping monitor with no data", "monitor_id", monitorID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Setup().Info("processing monitor series",
|
||||||
|
"monitor_id", monitorID,
|
||||||
|
"log_scores_count", len(logScores),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get monitor name from history.Monitors map or from monitor info
|
||||||
|
monitorName := "unknown"
|
||||||
|
if name, exists := history.Monitors[monitorID]; exists && name != "" {
|
||||||
|
monitorName = name
|
||||||
|
} else if monitor, exists := monitorInfo[monitorID]; exists {
|
||||||
|
monitorName = monitor.DisplayName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build target name and tags
|
||||||
|
sanitizedName := sanitizeMonitorName(monitorName)
|
||||||
|
target := "monitor{name=" + sanitizedName + "}"
|
||||||
|
|
||||||
|
tags := map[string]string{
|
||||||
|
"monitor_id": strconv.Itoa(monitorID),
|
||||||
|
"monitor_name": monitorName,
|
||||||
|
"type": "monitor",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add status (we'll use active as default since we have data for this monitor)
|
||||||
|
tags["status"] = "active"
|
||||||
|
|
||||||
|
// Define table columns
|
||||||
|
columns := []ColumnDef{
|
||||||
|
{Text: "time", Type: "time"},
|
||||||
|
{Text: "score", Type: "number"},
|
||||||
|
{Text: "rtt", Type: "number", Unit: "ms"},
|
||||||
|
{Text: "offset", Type: "number", Unit: "s"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build values array
|
||||||
|
var values [][]interface{}
|
||||||
|
for _, ls := range logScores {
|
||||||
|
// Convert timestamp to milliseconds
|
||||||
|
timestampMs := ls.Ts.Unix() * 1000
|
||||||
|
|
||||||
|
// Create row: [time, score, rtt, offset]
|
||||||
|
row := []interface{}{
|
||||||
|
timestampMs,
|
||||||
|
ls.Score,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add RTT (convert from microseconds to milliseconds, handle null)
|
||||||
|
if ls.Rtt.Valid {
|
||||||
|
rttMs := float64(ls.Rtt.Int32) / 1000.0
|
||||||
|
row = append(row, rttMs)
|
||||||
|
} else {
|
||||||
|
row = append(row, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add offset (handle null)
|
||||||
|
if ls.Offset.Valid {
|
||||||
|
row = append(row, ls.Offset.Float64)
|
||||||
|
} else {
|
||||||
|
row = append(row, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
values = append(values, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table series
|
||||||
|
series := GrafanaTableSeries{
|
||||||
|
Target: target,
|
||||||
|
Tags: tags,
|
||||||
|
Columns: columns,
|
||||||
|
Values: values,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = append(response, series)
|
||||||
|
|
||||||
|
logger.Setup().Info("created series for monitor",
|
||||||
|
"monitor_id", monitorID,
|
||||||
|
"target", series.Target,
|
||||||
|
"values_count", len(series.Values),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Setup().Info("transformation complete",
|
||||||
|
"final_response_count", len(response),
|
||||||
|
"response_is_nil", response == nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// scoresTimeRange handles Grafana time range requests for NTP server scores
|
||||||
|
func (srv *Server) scoresTimeRange(c echo.Context) error {
|
||||||
|
log := logger.Setup()
|
||||||
|
ctx, span := tracing.Tracer().Start(c.Request().Context(), "scoresTimeRange")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// Set reasonable default cache time; adjusted later based on data
|
||||||
|
c.Response().Header().Set("Cache-Control", "public,max-age=240")
|
||||||
|
|
||||||
|
// Validate mode parameter
|
||||||
|
mode := c.Param("mode")
|
||||||
|
if mode != "json" {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "invalid mode - only json supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and validate server first
|
||||||
|
server, err := srv.FindServer(ctx, c.Param("server"))
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorContext(ctx, "find server", "err", err)
|
||||||
|
if he, ok := err.(*echo.HTTPError); ok {
|
||||||
|
return he
|
||||||
|
}
|
||||||
|
span.RecordError(err)
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
|
||||||
|
}
|
||||||
|
if server.DeletionAge(30 * 24 * time.Hour) {
|
||||||
|
span.AddEvent("server deleted")
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "server not found")
|
||||||
|
}
|
||||||
|
if server.ID == 0 {
|
||||||
|
span.AddEvent("server not found")
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "server not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate time range parameters
|
||||||
|
params, err := srv.parseTimeRangeParams(ctx, c, server)
|
||||||
|
if err != nil {
|
||||||
|
if he, ok := err.(*echo.HTTPError); ok {
|
||||||
|
return he
|
||||||
|
}
|
||||||
|
log.ErrorContext(ctx, "parse time range parameters", "err", err)
|
||||||
|
span.RecordError(err)
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query ClickHouse for time range data
|
||||||
|
log.InfoContext(ctx, "executing clickhouse time range query",
|
||||||
|
"server_id", server.ID,
|
||||||
|
"server_ip", server.Ip,
|
||||||
|
"monitor_id", params.monitorID,
|
||||||
|
"from", params.from,
|
||||||
|
"to", params.to,
|
||||||
|
"max_data_points", params.maxDataPoints,
|
||||||
|
"time_range_duration", params.to.Sub(params.from).String(),
|
||||||
|
)
|
||||||
|
|
||||||
|
logScores, err := srv.ch.LogscoresTimeRange(ctx, int(server.ID), params.monitorID, params.from, params.to, params.maxDataPoints)
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorContext(ctx, "clickhouse time range query", "err", err,
|
||||||
|
"server_id", server.ID,
|
||||||
|
"monitor_id", params.monitorID,
|
||||||
|
"from", params.from,
|
||||||
|
"to", params.to,
|
||||||
|
)
|
||||||
|
span.RecordError(err)
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfoContext(ctx, "clickhouse query results",
|
||||||
|
"server_id", server.ID,
|
||||||
|
"rows_returned", len(logScores),
|
||||||
|
"first_few_ids", func() []uint64 {
|
||||||
|
ids := make([]uint64, 0, 3)
|
||||||
|
for i, ls := range logScores {
|
||||||
|
if i >= 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ids = append(ids, ls.ID)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build LogScoreHistory structure for compatibility with existing functions
|
||||||
|
history := &logscores.LogScoreHistory{
|
||||||
|
LogScores: logScores,
|
||||||
|
Monitors: make(map[int]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get monitor names for the returned data
|
||||||
|
monitorIDs := []uint32{}
|
||||||
|
for _, ls := range logScores {
|
||||||
|
if ls.MonitorID.Valid {
|
||||||
|
monitorID := uint32(ls.MonitorID.Int32)
|
||||||
|
if _, exists := history.Monitors[int(monitorID)]; !exists {
|
||||||
|
history.Monitors[int(monitorID)] = ""
|
||||||
|
monitorIDs = append(monitorIDs, monitorID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfoContext(ctx, "monitor processing",
|
||||||
|
"unique_monitor_ids", monitorIDs,
|
||||||
|
"monitor_count", len(monitorIDs),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get monitor details from database for status and display names
|
||||||
|
var monitors []ntpdb.Monitor
|
||||||
|
if len(monitorIDs) > 0 {
|
||||||
|
q := ntpdb.NewWrappedQuerier(ntpdb.New(srv.db))
|
||||||
|
logScoreMonitors, err := q.GetServerScores(ctx, ntpdb.GetServerScoresParams{
|
||||||
|
MonitorIDs: monitorIDs,
|
||||||
|
ServerID: server.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorContext(ctx, "get monitor details", "err", err)
|
||||||
|
// Don't fail the request, just use basic info
|
||||||
|
} else {
|
||||||
|
for _, lsm := range logScoreMonitors {
|
||||||
|
// Create monitor entry for transformation (we mainly need the display name)
|
||||||
|
tempMon := ntpdb.Monitor{
|
||||||
|
TlsName: lsm.TlsName,
|
||||||
|
Location: lsm.Location,
|
||||||
|
ID: lsm.ID,
|
||||||
|
}
|
||||||
|
monitors = append(monitors, tempMon)
|
||||||
|
|
||||||
|
// Update monitor name in history
|
||||||
|
history.Monitors[int(lsm.ID)] = tempMon.DisplayName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform to Grafana table format
|
||||||
|
log.InfoContext(ctx, "starting grafana transformation",
|
||||||
|
"log_scores_count", len(logScores),
|
||||||
|
"monitors_count", len(monitors),
|
||||||
|
"history_monitors", history.Monitors,
|
||||||
|
)
|
||||||
|
|
||||||
|
grafanaResponse := transformToGrafanaTableFormat(history, monitors)
|
||||||
|
|
||||||
|
log.InfoContext(ctx, "grafana transformation complete",
|
||||||
|
"response_series_count", len(grafanaResponse),
|
||||||
|
"response_preview", func() interface{} {
|
||||||
|
if len(grafanaResponse) == 0 {
|
||||||
|
return "empty_response"
|
||||||
|
}
|
||||||
|
first := grafanaResponse[0]
|
||||||
|
return map[string]interface{}{
|
||||||
|
"target": first.Target,
|
||||||
|
"tags": first.Tags,
|
||||||
|
"columns_count": len(first.Columns),
|
||||||
|
"values_count": len(first.Values),
|
||||||
|
"first_few_values": func() [][]interface{} {
|
||||||
|
if len(first.Values) == 0 {
|
||||||
|
return [][]interface{}{}
|
||||||
|
}
|
||||||
|
count := 2
|
||||||
|
if len(first.Values) < count {
|
||||||
|
count = len(first.Values)
|
||||||
|
}
|
||||||
|
return first.Values[:count]
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set cache control headers based on data characteristics
|
||||||
|
setHistoryCacheControl(c, history)
|
||||||
|
|
||||||
|
// Set CORS headers
|
||||||
|
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Response().Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
log.InfoContext(ctx, "time range response final",
|
||||||
|
"server_id", server.ID,
|
||||||
|
"server_ip", server.Ip,
|
||||||
|
"monitor_id", params.monitorID,
|
||||||
|
"time_range", params.to.Sub(params.from).String(),
|
||||||
|
"raw_data_points", len(logScores),
|
||||||
|
"grafana_series_count", len(grafanaResponse),
|
||||||
|
"max_data_points", params.maxDataPoints,
|
||||||
|
"response_is_null", grafanaResponse == nil,
|
||||||
|
"response_is_empty", len(grafanaResponse) == 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, grafanaResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testGrafanaTable returns sample data in Grafana table format for validation
|
||||||
|
func (srv *Server) testGrafanaTable(c echo.Context) error {
|
||||||
|
log := logger.Setup()
|
||||||
|
ctx, span := tracing.Tracer().Start(c.Request().Context(), "testGrafanaTable")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
log.InfoContext(ctx, "serving test Grafana table format",
|
||||||
|
"remote_ip", c.RealIP(),
|
||||||
|
"user_agent", c.Request().UserAgent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate sample data with realistic NTP Pool values
|
||||||
|
now := time.Now()
|
||||||
|
sampleData := GrafanaTimeSeriesResponse{
|
||||||
|
{
|
||||||
|
Target: "monitor{name=zakim1-yfhw4a}",
|
||||||
|
Tags: map[string]string{
|
||||||
|
"monitor_id": "126",
|
||||||
|
"monitor_name": "zakim1-yfhw4a",
|
||||||
|
"type": "monitor",
|
||||||
|
"status": "active",
|
||||||
|
},
|
||||||
|
Columns: []ColumnDef{
|
||||||
|
{Text: "time", Type: "time"},
|
||||||
|
{Text: "score", Type: "number"},
|
||||||
|
{Text: "rtt", Type: "number", Unit: "ms"},
|
||||||
|
{Text: "offset", Type: "number", Unit: "s"},
|
||||||
|
},
|
||||||
|
Values: [][]interface{}{
|
||||||
|
{now.Add(-10*time.Minute).Unix() * 1000, 20.0, 18.865, -0.000267},
|
||||||
|
{now.Add(-20*time.Minute).Unix() * 1000, 20.0, 18.96, -0.000390},
|
||||||
|
{now.Add(-30*time.Minute).Unix() * 1000, 20.0, 18.073, -0.000768},
|
||||||
|
{now.Add(-40*time.Minute).Unix() * 1000, 20.0, 18.209, nil}, // null offset example
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Target: "monitor{name=nj2-mon01}",
|
||||||
|
Tags: map[string]string{
|
||||||
|
"monitor_id": "84",
|
||||||
|
"monitor_name": "nj2-mon01",
|
||||||
|
"type": "monitor",
|
||||||
|
"status": "active",
|
||||||
|
},
|
||||||
|
Columns: []ColumnDef{
|
||||||
|
{Text: "time", Type: "time"},
|
||||||
|
{Text: "score", Type: "number"},
|
||||||
|
{Text: "rtt", Type: "number", Unit: "ms"},
|
||||||
|
{Text: "offset", Type: "number", Unit: "s"},
|
||||||
|
},
|
||||||
|
Values: [][]interface{}{
|
||||||
|
{now.Add(-10*time.Minute).Unix() * 1000, 19.5, 22.145, 0.000123},
|
||||||
|
{now.Add(-20*time.Minute).Unix() * 1000, 19.8, 21.892, 0.000089},
|
||||||
|
{now.Add(-30*time.Minute).Unix() * 1000, 20.0, 22.034, 0.000156},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CORS header for browser testing
|
||||||
|
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Response().Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Set cache control similar to other endpoints
|
||||||
|
c.Response().Header().Set("Cache-Control", "public,max-age=60")
|
||||||
|
|
||||||
|
log.InfoContext(ctx, "test Grafana table response sent",
|
||||||
|
"series_count", len(sampleData),
|
||||||
|
"response_size_approx", "~1KB",
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, sampleData)
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ type historyParameters struct {
|
|||||||
fullHistory bool
|
fullHistory bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Server) getHistoryParameters(ctx context.Context, c echo.Context) (historyParameters, error) {
|
func (srv *Server) getHistoryParameters(ctx context.Context, c echo.Context, server ntpdb.Server) (historyParameters, error) {
|
||||||
log := logger.FromContext(ctx)
|
log := logger.FromContext(ctx)
|
||||||
|
|
||||||
p := historyParameters{}
|
p := historyParameters{}
|
||||||
@@ -94,9 +94,18 @@ func (srv *Server) getHistoryParameters(ctx context.Context, c echo.Context) (hi
|
|||||||
switch monitorParam {
|
switch monitorParam {
|
||||||
case "":
|
case "":
|
||||||
name := "recentmedian.scores.ntp.dev"
|
name := "recentmedian.scores.ntp.dev"
|
||||||
monitor, err := q.GetMonitorByName(ctx, sql.NullString{Valid: true, String: name})
|
var ipVersion ntpdb.NullMonitorsIpVersion
|
||||||
|
if server.IpVersion == ntpdb.ServersIpVersionV4 {
|
||||||
|
ipVersion = ntpdb.NullMonitorsIpVersion{MonitorsIpVersion: ntpdb.MonitorsIpVersionV4, Valid: true}
|
||||||
|
} else {
|
||||||
|
ipVersion = ntpdb.NullMonitorsIpVersion{MonitorsIpVersion: ntpdb.MonitorsIpVersionV6, Valid: true}
|
||||||
|
}
|
||||||
|
monitor, err := q.GetMonitorByNameAndIPVersion(ctx, ntpdb.GetMonitorByNameAndIPVersionParams{
|
||||||
|
TlsName: sql.NullString{Valid: true, String: name},
|
||||||
|
IpVersion: ipVersion,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("could not find monitor", "name", name, "err", err)
|
log.Warn("could not find monitor", "name", name, "ip_version", server.IpVersion, "err", err)
|
||||||
}
|
}
|
||||||
monitorID = monitor.ID
|
monitorID = monitor.ID
|
||||||
case "*":
|
case "*":
|
||||||
@@ -113,12 +122,21 @@ func (srv *Server) getHistoryParameters(ctx context.Context, c echo.Context) (hi
|
|||||||
}
|
}
|
||||||
|
|
||||||
monitorParam = monitorParam + ".%"
|
monitorParam = monitorParam + ".%"
|
||||||
monitor, err := q.GetMonitorByName(ctx, sql.NullString{Valid: true, String: monitorParam})
|
var ipVersion ntpdb.NullMonitorsIpVersion
|
||||||
|
if server.IpVersion == ntpdb.ServersIpVersionV4 {
|
||||||
|
ipVersion = ntpdb.NullMonitorsIpVersion{MonitorsIpVersion: ntpdb.MonitorsIpVersionV4, Valid: true}
|
||||||
|
} else {
|
||||||
|
ipVersion = ntpdb.NullMonitorsIpVersion{MonitorsIpVersion: ntpdb.MonitorsIpVersionV6, Valid: true}
|
||||||
|
}
|
||||||
|
monitor, err := q.GetMonitorByNameAndIPVersion(ctx, ntpdb.GetMonitorByNameAndIPVersionParams{
|
||||||
|
TlsName: sql.NullString{Valid: true, String: monitorParam},
|
||||||
|
IpVersion: ipVersion,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return p, echo.NewHTTPError(http.StatusNotFound, "monitor not found").WithInternal(err)
|
return p, echo.NewHTTPError(http.StatusNotFound, "monitor not found").WithInternal(err)
|
||||||
}
|
}
|
||||||
log.WarnContext(ctx, "could not find monitor", "name", monitorParam, "err", err)
|
log.WarnContext(ctx, "could not find monitor", "name", monitorParam, "ip_version", server.IpVersion, "err", err)
|
||||||
return p, echo.NewHTTPError(http.StatusNotFound, "monitor not found (sql)")
|
return p, echo.NewHTTPError(http.StatusNotFound, "monitor not found (sql)")
|
||||||
}
|
}
|
||||||
monitorID = monitor.ID
|
monitorID = monitor.ID
|
||||||
@@ -127,7 +145,7 @@ func (srv *Server) getHistoryParameters(ctx context.Context, c echo.Context) (hi
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.monitorID = int(monitorID)
|
p.monitorID = int(monitorID)
|
||||||
log.DebugContext(ctx, "monitor param", "monitor", monitorID)
|
log.DebugContext(ctx, "monitor param", "monitor", monitorID, "ip_version", server.IpVersion)
|
||||||
|
|
||||||
since, _ := strconv.ParseInt(c.QueryParam("since"), 10, 64) // defaults to 0 so don't care if it parses
|
since, _ := strconv.ParseInt(c.QueryParam("since"), 10, 64) // defaults to 0 so don't care if it parses
|
||||||
if since > 0 {
|
if since > 0 {
|
||||||
@@ -171,16 +189,6 @@ func (srv *Server) history(c echo.Context) error {
|
|||||||
return echo.NewHTTPError(http.StatusNotFound, "invalid mode")
|
return echo.NewHTTPError(http.StatusNotFound, "invalid mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := srv.getHistoryParameters(ctx, c)
|
|
||||||
if err != nil {
|
|
||||||
if he, ok := err.(*echo.HTTPError); ok {
|
|
||||||
return he
|
|
||||||
}
|
|
||||||
log.ErrorContext(ctx, "get history parameters", "err", err)
|
|
||||||
span.RecordError(err)
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
|
|
||||||
}
|
|
||||||
|
|
||||||
server, err := srv.FindServer(ctx, c.Param("server"))
|
server, err := srv.FindServer(ctx, c.Param("server"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorContext(ctx, "find server", "err", err)
|
log.ErrorContext(ctx, "find server", "err", err)
|
||||||
@@ -199,6 +207,16 @@ func (srv *Server) history(c echo.Context) error {
|
|||||||
return echo.NewHTTPError(http.StatusNotFound, "server not found")
|
return echo.NewHTTPError(http.StatusNotFound, "server not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p, err := srv.getHistoryParameters(ctx, c, server)
|
||||||
|
if err != nil {
|
||||||
|
if he, ok := err.(*echo.HTTPError); ok {
|
||||||
|
return he
|
||||||
|
}
|
||||||
|
log.ErrorContext(ctx, "get history parameters", "err", err)
|
||||||
|
span.RecordError(err)
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
|
||||||
|
}
|
||||||
|
|
||||||
p.server = server
|
p.server = server
|
||||||
|
|
||||||
var history *logscores.LogScoreHistory
|
var history *logscores.LogScoreHistory
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func (srv *Server) Run() error {
|
|||||||
|
|
||||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||||
AllowOrigins: []string{
|
AllowOrigins: []string{
|
||||||
"http://localhost", "http://localhost:5173", "http://localhost:8080",
|
"http://localhost", "http://localhost:5173", "http://localhost:5174", "http://localhost:8080",
|
||||||
"https://www.ntppool.org", "https://*.ntppool.org",
|
"https://www.ntppool.org", "https://*.ntppool.org",
|
||||||
"https://web.beta.grundclock.com", "https://manage.beta.grundclock.com",
|
"https://web.beta.grundclock.com", "https://manage.beta.grundclock.com",
|
||||||
"https:/*.askdev.grundclock.com",
|
"https:/*.askdev.grundclock.com",
|
||||||
@@ -208,6 +208,8 @@ func (srv *Server) Run() error {
|
|||||||
e.GET("/api/server/dns/answers/:server", srv.dnsAnswers)
|
e.GET("/api/server/dns/answers/:server", srv.dnsAnswers)
|
||||||
e.GET("/api/server/scores/:server/:mode", srv.history)
|
e.GET("/api/server/scores/:server/:mode", srv.history)
|
||||||
e.GET("/api/dns/counts", srv.dnsQueryCounts)
|
e.GET("/api/dns/counts", srv.dnsQueryCounts)
|
||||||
|
e.GET("/api/v2/test/grafana-table", srv.testGrafanaTable)
|
||||||
|
e.GET("/api/v2/server/scores/:server/:mode", srv.scoresTimeRange)
|
||||||
|
|
||||||
if len(ntpconf.WebHostname()) > 0 {
|
if len(ntpconf.WebHostname()) > 0 {
|
||||||
e.POST("/api/server/scores/:server/:mode", func(c echo.Context) error {
|
e.POST("/api/server/scores/:server/:mode", func(c echo.Context) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user