2022-07-26 10:03:02 +02:00
|
|
|
package apiv1
|
2021-08-20 12:54:11 +02:00
|
|
|
|
|
|
|
import (
|
2021-10-11 16:28:05 +02:00
|
|
|
"bufio"
|
2021-08-20 12:54:11 +02:00
|
|
|
"context"
|
2021-09-20 09:27:31 +02:00
|
|
|
"crypto/ed25519"
|
|
|
|
"encoding/base64"
|
2021-08-20 12:54:11 +02:00
|
|
|
"encoding/json"
|
2021-09-20 09:27:31 +02:00
|
|
|
"errors"
|
2022-01-24 09:55:33 +01:00
|
|
|
"io"
|
2021-08-20 12:54:11 +02:00
|
|
|
"log"
|
2021-11-26 09:51:18 +01:00
|
|
|
"math"
|
2021-08-20 12:54:11 +02:00
|
|
|
"net/http"
|
2021-09-20 09:27:31 +02:00
|
|
|
"strings"
|
2022-03-14 08:50:28 +01:00
|
|
|
"sync"
|
2021-08-20 12:54:11 +02:00
|
|
|
"time"
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
"github.com/ClusterCockpit/cc-metric-store/internal/api"
|
|
|
|
"github.com/ClusterCockpit/cc-metric-store/internal/memstore"
|
|
|
|
"github.com/ClusterCockpit/cc-metric-store/internal/types"
|
2021-09-20 09:27:31 +02:00
|
|
|
"github.com/golang-jwt/jwt/v4"
|
2021-08-20 12:54:11 +02:00
|
|
|
"github.com/gorilla/mux"
|
2021-10-11 16:28:05 +02:00
|
|
|
"github.com/influxdata/line-protocol/v2/lineprotocol"
|
2021-08-20 12:54:11 +02:00
|
|
|
)
|
|
|
|
|
2021-08-31 15:17:36 +02:00
|
|
|
type ApiMetricData struct {
|
2022-07-26 10:03:02 +02:00
|
|
|
Error *string `json:"error,omitempty"`
|
|
|
|
From int64 `json:"from"`
|
|
|
|
To int64 `json:"to"`
|
|
|
|
Data types.FloatArray `json:"data,omitempty"`
|
|
|
|
Avg types.Float `json:"avg"`
|
|
|
|
Min types.Float `json:"min"`
|
|
|
|
Max types.Float `json:"max"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type HttpApi struct {
|
|
|
|
MemoryStore *memstore.MemoryStore
|
|
|
|
server *http.Server
|
|
|
|
PublicKey string
|
|
|
|
Address string
|
|
|
|
CertFile, KeyFile string
|
2021-11-26 09:51:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Optimize this, just like the stats endpoint!
|
|
|
|
func (data *ApiMetricData) AddStats() {
|
|
|
|
n := 0
|
2021-12-15 09:59:33 +01:00
|
|
|
sum, min, max := 0.0, math.MaxFloat64, -math.MaxFloat64
|
2021-11-26 09:51:18 +01:00
|
|
|
for _, x := range data.Data {
|
|
|
|
if x.IsNaN() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
n += 1
|
|
|
|
sum += float64(x)
|
|
|
|
min = math.Min(min, float64(x))
|
|
|
|
max = math.Max(max, float64(x))
|
|
|
|
}
|
|
|
|
|
2021-12-15 09:59:33 +01:00
|
|
|
if n > 0 {
|
|
|
|
avg := sum / float64(n)
|
2022-07-26 10:03:02 +02:00
|
|
|
data.Avg = types.Float(avg)
|
|
|
|
data.Min = types.Float(min)
|
|
|
|
data.Max = types.Float(max)
|
2021-12-15 09:59:33 +01:00
|
|
|
} else {
|
2022-07-26 10:03:02 +02:00
|
|
|
data.Avg, data.Min, data.Max = types.NaN, types.NaN, types.NaN
|
2021-12-15 09:59:33 +01:00
|
|
|
}
|
2021-08-20 12:54:11 +02:00
|
|
|
}
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
func (data *ApiMetricData) ScaleBy(f types.Float) {
|
2022-03-31 14:17:27 +02:00
|
|
|
if f == 0 || f == 1 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data.Avg *= f
|
|
|
|
data.Min *= f
|
|
|
|
data.Max *= f
|
|
|
|
for i := 0; i < len(data.Data); i++ {
|
|
|
|
data.Data[i] *= f
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
func (ha *HttpApi) padWithNaNs(data *ApiMetricData, metric string, from, to int64) {
|
|
|
|
mc, ok := ha.MemoryStore.GetMetricConf(metric)
|
2022-01-31 16:32:50 +01:00
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
if (data.From / mc.Frequency) > (from / mc.Frequency) {
|
|
|
|
padfront := int((data.From / mc.Frequency) - (from / mc.Frequency))
|
|
|
|
ndata := make([]types.Float, 0, padfront+len(data.Data))
|
2022-01-31 16:32:50 +01:00
|
|
|
for i := 0; i < padfront; i++ {
|
2022-07-26 10:03:02 +02:00
|
|
|
ndata = append(ndata, types.NaN)
|
2022-01-31 16:32:50 +01:00
|
|
|
}
|
|
|
|
for j := 0; j < len(data.Data); j++ {
|
|
|
|
ndata = append(ndata, data.Data[j])
|
|
|
|
}
|
|
|
|
data.Data = ndata
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
func (ha *HttpApi) handleWrite(rw http.ResponseWriter, r *http.Request) {
|
2021-10-11 16:28:05 +02:00
|
|
|
if r.Method != http.MethodPost {
|
|
|
|
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-24 09:55:33 +01:00
|
|
|
bytes, err := io.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
2022-04-01 14:01:43 +02:00
|
|
|
log.Printf("error while reading request body: %s", err.Error())
|
2022-01-24 09:55:33 +01:00
|
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dec := lineprotocol.NewDecoderWithBytes(bytes)
|
2022-07-26 10:03:02 +02:00
|
|
|
if err := api.DecodeLine(ha.MemoryStore, dec, r.URL.Query().Get("cluster")); err != nil {
|
2022-03-08 09:27:44 +01:00
|
|
|
log.Printf("/api/write error: %s", err.Error())
|
2021-10-11 16:28:05 +02:00
|
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
}
|
|
|
|
|
2022-01-07 08:52:55 +01:00
|
|
|
type ApiQueryRequest struct {
|
2022-01-20 10:42:44 +01:00
|
|
|
Cluster string `json:"cluster"`
|
|
|
|
From int64 `json:"from"`
|
|
|
|
To int64 `json:"to"`
|
|
|
|
WithStats bool `json:"with-stats"`
|
|
|
|
WithData bool `json:"with-data"`
|
2022-01-31 16:32:50 +01:00
|
|
|
WithPadding bool `json:"with-padding"`
|
2022-01-20 10:42:44 +01:00
|
|
|
Queries []ApiQuery `json:"queries"`
|
|
|
|
ForAllNodes []string `json:"for-all-nodes"`
|
2022-01-07 08:52:55 +01:00
|
|
|
}
|
|
|
|
|
2022-02-02 11:26:05 +01:00
|
|
|
type ApiQueryResponse struct {
|
|
|
|
Queries []ApiQuery `json:"queries,omitempty"`
|
|
|
|
Results [][]ApiMetricData `json:"results"`
|
|
|
|
}
|
|
|
|
|
2022-01-07 08:52:55 +01:00
|
|
|
type ApiQuery struct {
|
2022-07-26 10:03:02 +02:00
|
|
|
Metric string `json:"metric"`
|
|
|
|
Hostname string `json:"host"`
|
|
|
|
Aggregate bool `json:"aggreg"`
|
|
|
|
ScaleFactor types.Float `json:"scale-by,omitempty"`
|
|
|
|
Type *string `json:"type,omitempty"`
|
|
|
|
TypeIds []string `json:"type-ids,omitempty"`
|
|
|
|
SubType *string `json:"subtype,omitempty"`
|
|
|
|
SubTypeIds []string `json:"subtype-ids,omitempty"`
|
2022-01-07 08:52:55 +01:00
|
|
|
}
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
func (ha *HttpApi) handleQuery(rw http.ResponseWriter, r *http.Request) {
|
2022-01-07 08:52:55 +01:00
|
|
|
var err error
|
2022-02-02 11:45:52 +01:00
|
|
|
var req ApiQueryRequest = ApiQueryRequest{WithStats: true, WithData: true, WithPadding: true}
|
2022-01-07 08:52:55 +01:00
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-02-02 11:26:05 +01:00
|
|
|
response := ApiQueryResponse{
|
|
|
|
Results: make([][]ApiMetricData, 0, len(req.Queries)),
|
|
|
|
}
|
2022-01-20 10:42:44 +01:00
|
|
|
if req.ForAllNodes != nil {
|
2022-07-26 10:03:02 +02:00
|
|
|
nodes := ha.MemoryStore.ListChildren([]string{req.Cluster})
|
2022-01-20 10:42:44 +01:00
|
|
|
for _, node := range nodes {
|
|
|
|
for _, metric := range req.ForAllNodes {
|
2022-02-02 11:26:05 +01:00
|
|
|
q := ApiQuery{
|
2022-01-20 10:42:44 +01:00
|
|
|
Metric: metric,
|
|
|
|
Hostname: node,
|
2022-02-02 11:26:05 +01:00
|
|
|
}
|
|
|
|
req.Queries = append(req.Queries, q)
|
|
|
|
response.Queries = append(response.Queries, q)
|
2022-01-07 08:52:55 +01:00
|
|
|
}
|
2022-01-20 10:42:44 +01:00
|
|
|
}
|
|
|
|
}
|
2022-01-07 08:52:55 +01:00
|
|
|
|
2022-01-20 10:42:44 +01:00
|
|
|
for _, query := range req.Queries {
|
2022-07-26 10:03:02 +02:00
|
|
|
sels := make([]types.Selector, 0, 1)
|
2022-01-20 10:42:44 +01:00
|
|
|
if query.Aggregate || query.Type == nil {
|
2022-07-26 10:03:02 +02:00
|
|
|
sel := types.Selector{{String: req.Cluster}, {String: query.Hostname}}
|
2022-01-20 10:42:44 +01:00
|
|
|
if query.Type != nil {
|
|
|
|
if len(query.TypeIds) == 1 {
|
2022-07-26 10:03:02 +02:00
|
|
|
sel = append(sel, types.SelectorElement{String: *query.Type + query.TypeIds[0]})
|
2022-01-07 08:52:55 +01:00
|
|
|
} else {
|
2022-01-20 10:42:44 +01:00
|
|
|
ids := make([]string, len(query.TypeIds))
|
|
|
|
for i, id := range query.TypeIds {
|
2022-05-04 09:18:56 +02:00
|
|
|
ids[i] = *query.Type + id
|
2022-01-07 08:52:55 +01:00
|
|
|
}
|
2022-07-26 10:03:02 +02:00
|
|
|
sel = append(sel, types.SelectorElement{Group: ids})
|
2022-01-07 08:52:55 +01:00
|
|
|
}
|
2022-01-20 10:42:44 +01:00
|
|
|
|
|
|
|
if query.SubType != nil {
|
|
|
|
if len(query.SubTypeIds) == 1 {
|
2022-07-26 10:03:02 +02:00
|
|
|
sel = append(sel, types.SelectorElement{String: *query.SubType + query.SubTypeIds[0]})
|
2022-01-20 10:42:44 +01:00
|
|
|
} else {
|
|
|
|
ids := make([]string, len(query.SubTypeIds))
|
|
|
|
for i, id := range query.SubTypeIds {
|
2022-05-04 09:18:56 +02:00
|
|
|
ids[i] = *query.SubType + id
|
2022-01-20 10:42:44 +01:00
|
|
|
}
|
2022-07-26 10:03:02 +02:00
|
|
|
sel = append(sel, types.SelectorElement{Group: ids})
|
2022-01-20 10:42:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sels = append(sels, sel)
|
|
|
|
} else {
|
|
|
|
for _, typeId := range query.TypeIds {
|
|
|
|
if query.SubType != nil {
|
|
|
|
for _, subTypeId := range query.SubTypeIds {
|
2022-07-26 10:03:02 +02:00
|
|
|
sels = append(sels, types.Selector{
|
2022-01-20 10:42:44 +01:00
|
|
|
{String: req.Cluster}, {String: query.Hostname},
|
2022-05-04 09:18:56 +02:00
|
|
|
{String: *query.Type + typeId},
|
|
|
|
{String: *query.SubType + subTypeId}})
|
2022-01-20 10:42:44 +01:00
|
|
|
}
|
|
|
|
} else {
|
2022-07-26 10:03:02 +02:00
|
|
|
sels = append(sels, types.Selector{
|
2022-01-20 10:42:44 +01:00
|
|
|
{String: req.Cluster},
|
|
|
|
{String: query.Hostname},
|
2022-05-04 09:18:56 +02:00
|
|
|
{String: *query.Type + typeId}})
|
2022-01-20 10:42:44 +01:00
|
|
|
}
|
2022-01-07 08:52:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-02 11:45:52 +01:00
|
|
|
// log.Printf("query: %#v\n", query)
|
|
|
|
// log.Printf("sels: %#v\n", sels)
|
|
|
|
|
2022-01-20 10:42:44 +01:00
|
|
|
res := make([]ApiMetricData, 0, len(sels))
|
|
|
|
for _, sel := range sels {
|
|
|
|
data := ApiMetricData{}
|
2022-07-26 10:03:02 +02:00
|
|
|
data.Data, data.From, data.To, err = ha.MemoryStore.Read(sel, query.Metric, req.From, req.To)
|
2022-02-02 11:45:52 +01:00
|
|
|
// log.Printf("data: %#v, %#v, %#v, %#v", data.Data, data.From, data.To, err)
|
2022-01-20 10:42:44 +01:00
|
|
|
if err != nil {
|
|
|
|
msg := err.Error()
|
|
|
|
data.Error = &msg
|
2022-02-02 11:45:52 +01:00
|
|
|
res = append(res, data)
|
2022-01-20 10:42:44 +01:00
|
|
|
continue
|
|
|
|
}
|
2022-01-07 08:52:55 +01:00
|
|
|
|
2022-01-20 10:42:44 +01:00
|
|
|
if req.WithStats {
|
|
|
|
data.AddStats()
|
|
|
|
}
|
2022-03-31 14:17:27 +02:00
|
|
|
if query.ScaleFactor != 0 {
|
|
|
|
data.ScaleBy(query.ScaleFactor)
|
|
|
|
}
|
2022-01-31 16:32:50 +01:00
|
|
|
if req.WithPadding {
|
2022-07-26 10:03:02 +02:00
|
|
|
ha.padWithNaNs(&data, query.Metric, req.From, req.To)
|
2022-01-31 16:32:50 +01:00
|
|
|
}
|
2022-01-20 10:42:44 +01:00
|
|
|
if !req.WithData {
|
|
|
|
data.Data = nil
|
|
|
|
}
|
|
|
|
res = append(res, data)
|
|
|
|
}
|
2022-02-02 11:26:05 +01:00
|
|
|
response.Results = append(response.Results, res)
|
2022-01-07 08:52:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
|
|
bw := bufio.NewWriter(rw)
|
|
|
|
defer bw.Flush()
|
|
|
|
if err := json.NewEncoder(bw).Encode(response); err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2021-11-22 17:04:09 +01:00
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
func (ha *HttpApi) authentication(next http.Handler, publicKey ed25519.PublicKey) http.Handler {
|
2022-03-14 08:50:28 +01:00
|
|
|
cacheLock := sync.RWMutex{}
|
|
|
|
cache := map[string]*jwt.Token{}
|
|
|
|
|
2021-09-20 09:27:31 +02:00
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
authheader := r.Header.Get("Authorization")
|
|
|
|
if authheader == "" || !strings.HasPrefix(authheader, "Bearer ") {
|
|
|
|
http.Error(rw, "Use JWT Authentication", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-14 08:50:28 +01:00
|
|
|
rawtoken := authheader[len("Bearer "):]
|
|
|
|
cacheLock.RLock()
|
|
|
|
token, ok := cache[rawtoken]
|
|
|
|
cacheLock.RUnlock()
|
|
|
|
if ok && token.Claims.Valid() == nil {
|
|
|
|
next.ServeHTTP(rw, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-20 09:27:31 +02:00
|
|
|
// The actual token is ignored for now.
|
|
|
|
// In case expiration and so on are specified, the Parse function
|
|
|
|
// already returns an error for expired tokens.
|
2022-03-14 08:50:28 +01:00
|
|
|
var err error
|
|
|
|
token, err = jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
|
2021-09-20 09:27:31 +02:00
|
|
|
if t.Method != jwt.SigningMethodEdDSA {
|
|
|
|
return nil, errors.New("only Ed25519/EdDSA supported")
|
|
|
|
}
|
|
|
|
|
|
|
|
return publicKey, nil
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-14 08:50:28 +01:00
|
|
|
cacheLock.Lock()
|
|
|
|
cache[rawtoken] = token
|
|
|
|
cacheLock.Unlock()
|
|
|
|
|
2021-09-20 09:27:31 +02:00
|
|
|
// Let request through...
|
|
|
|
next.ServeHTTP(rw, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
func (ha *HttpApi) StartServer(ctx context.Context) error {
|
2021-08-20 12:54:11 +02:00
|
|
|
r := mux.NewRouter()
|
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
r.HandleFunc("/api/write", ha.handleWrite)
|
|
|
|
r.HandleFunc("/api/query", ha.handleQuery)
|
2022-02-04 08:30:50 +01:00
|
|
|
r.HandleFunc("/api/debug", func(rw http.ResponseWriter, r *http.Request) {
|
2022-03-31 14:17:27 +02:00
|
|
|
raw := r.URL.Query().Get("selector")
|
|
|
|
selector := []string{}
|
|
|
|
if len(raw) != 0 {
|
|
|
|
selector = strings.Split(raw, ":")
|
|
|
|
}
|
2022-02-04 08:30:50 +01:00
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
if err := ha.MemoryStore.DebugDump(bufio.NewWriter(rw), selector); err != nil {
|
2022-03-31 14:17:27 +02:00
|
|
|
rw.WriteHeader(http.StatusBadRequest)
|
|
|
|
rw.Write([]byte(err.Error()))
|
|
|
|
}
|
2022-02-04 08:30:50 +01:00
|
|
|
})
|
2021-08-20 12:54:11 +02:00
|
|
|
|
|
|
|
server := &http.Server{
|
|
|
|
Handler: r,
|
2022-07-26 10:03:02 +02:00
|
|
|
Addr: ha.Address,
|
2022-03-31 14:17:27 +02:00
|
|
|
WriteTimeout: 30 * time.Second,
|
|
|
|
ReadTimeout: 30 * time.Second,
|
2021-08-20 12:54:11 +02:00
|
|
|
}
|
2022-07-26 10:03:02 +02:00
|
|
|
ha.server = server
|
2021-08-20 12:54:11 +02:00
|
|
|
|
2022-07-26 10:03:02 +02:00
|
|
|
if len(ha.PublicKey) > 0 {
|
|
|
|
buf, err := base64.StdEncoding.DecodeString(ha.PublicKey)
|
2021-09-20 09:27:31 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
publicKey := ed25519.PublicKey(buf)
|
2022-07-26 10:03:02 +02:00
|
|
|
server.Handler = ha.authentication(server.Handler, publicKey)
|
2021-09-20 09:27:31 +02:00
|
|
|
}
|
|
|
|
|
2021-08-20 12:54:11 +02:00
|
|
|
go func() {
|
2022-07-26 10:03:02 +02:00
|
|
|
if ha.CertFile != "" && ha.KeyFile != "" {
|
|
|
|
log.Printf("API https endpoint listening on '%s'\n", ha.Address)
|
|
|
|
err := server.ListenAndServeTLS(ha.CertFile, ha.KeyFile)
|
2022-02-04 08:30:50 +01:00
|
|
|
if err != nil && err != http.ErrServerClosed {
|
|
|
|
log.Println(err)
|
|
|
|
}
|
|
|
|
} else {
|
2022-07-26 10:03:02 +02:00
|
|
|
log.Printf("API http endpoint listening on '%s'\n", ha.Address)
|
2022-02-04 08:30:50 +01:00
|
|
|
err := server.ListenAndServe()
|
|
|
|
if err != nil && err != http.ErrServerClosed {
|
|
|
|
log.Println(err)
|
|
|
|
}
|
2021-08-20 12:54:11 +02:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
for {
|
2021-09-20 09:27:31 +02:00
|
|
|
<-ctx.Done()
|
2021-08-20 12:54:11 +02:00
|
|
|
err := server.Shutdown(context.Background())
|
|
|
|
log.Println("API server shut down")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|