<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/clear</command-name>
            <command-message>clear</command-message>
            <command-args></command-args>


<local-command-stdout></local-command-stdout>


<command-name>/mcp</command-name>
            <command-message>mcp</command-message>
            <command-args></command-args>


<local-command-stdout>MCP dialog dismissed</local-command-stdout>


Integrate swaggo/swag  and update to latest release


The user wants to integrate swaggo/swag and update to the latest release. Let me first check the current state of the project to understand what's already there.


ToolSearch

Question: select:Read,Glob,Grep,Bash


Question: select:Read,Glob,Grep,Bash


     1→module github.com/ClusterCockpit/cc-metric-store
     2→
     3→go 1.25.0
     4→
     5→require (
     6→	github.com/ClusterCockpit/cc-backend v1.5.0
     7→	github.com/ClusterCockpit/cc-lib/v2 v2.8.0
     8→	github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0
     9→	github.com/golang-jwt/jwt/v4 v4.5.2
    10→	github.com/google/gops v0.3.29
    11→	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
    12→	github.com/swaggo/http-swagger v1.3.4
    13→	github.com/swaggo/swag v1.16.6
    14→)
    15→
    16→require (
    17→	github.com/KyleBanks/depth v1.2.1 // indirect
    18→	github.com/andybalholm/brotli v1.2.0 // indirect
    19→	github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
    20→	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
    21→	github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
    22→	github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
    23→	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
    24→	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
    25→	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
    26→	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
    27→	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
    28→	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
    29→	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
    30→	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
    31→	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
    32→	github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 // indirect
    33→	github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
    34→	github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
    35→	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
    36→	github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
    37→	github.com/aws/smithy-go v1.24.2 // indirect
    38→	github.com/fsnotify/fsnotify v1.9.0 // indirect
    39→	github.com/go-openapi/jsonpointer v0.22.5 // indirect
    40→	github.com/go-openapi/jsonreference v0.21.5 // indirect
    41→	github.com/go-openapi/spec v0.22.4 // indirect
    42→	github.com/go-openapi/swag/conv v0.25.5 // indirect
    43→	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
    44→	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
    45→	github.com/go-openapi/swag/loading v0.25.5 // indirect
    46→	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
    47→	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
    48→	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
    49→	github.com/google/uuid v1.6.0 // indirect
    50→	github.com/klauspost/compress v1.18.4 // indirect
    51→	github.com/mattn/go-sqlite3 v1.14.34 // indirect
    52→	github.com/nats-io/nats.go v1.49.0 // indirect
    53→	github.com/nats-io/nkeys v0.4.15 // indirect
    54→	github.com/nats-io/nuid v1.0.1 // indirect
    55→	github.com/parquet-go/bitpack v1.0.0 // indirect
    56→	github.com/parquet-go/jsonlite v1.4.0 // indirect
    57→	github.com/parquet-go/parquet-go v0.28.0 // indirect
    58→	github.com/pierrec/lz4/v4 v4.1.26 // indirect
    59→	github.com/swaggo/files v1.0.1 // indirect
    60→	github.com/twpayne/go-geom v1.6.1 // indirect
    61→	go.yaml.in/yaml/v3 v3.0.4 // indirect
    62→	golang.org/x/crypto v0.48.0 // indirect
    63→	golang.org/x/mod v0.33.0 // indirect
    64→	golang.org/x/net v0.51.0 // indirect
    65→	golang.org/x/sync v0.19.0 // indirect
    66→	golang.org/x/sys v0.41.0 // indirect
    67→	golang.org/x/tools v0.42.0 // indirect
    68→	google.golang.org/protobuf v1.36.11 // indirect
    69→	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
    70→)
    71→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

module github.com/ClusterCockpit/cc-metric-store

go 1.25.0

require (
	github.com/ClusterCockpit/cc-backend v1.5.0
	github.com/ClusterCockpit/cc-lib/v2 v2.8.0
	github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0
	github.com/golang-jwt/jwt/v4 v4.5.2
	github.com/google/gops v0.3.29
	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
	github.com/swaggo/http-swagger v1.3.4
	github.com/swaggo/swag v1.16.6
)

require (
	github.com/KyleBanks/depth v1.2.1 // indirect
	github.com/andybalholm/brotli v1.2.0 // indirect
	github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
	github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
	github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
	github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 // indirect
	github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
	github.com/aws/smithy-go v1.24.2 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/go-openapi/jsonpointer v0.22.5 // indirect
	github.com/go-openapi/jsonreference v0.21.5 // indirect
	github.com/go-openapi/spec v0.22.4 // indirect
	github.com/go-openapi/swag/conv v0.25.5 // indirect
	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
	github.com/go-openapi/swag/loading v0.25.5 // indirect
	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/klauspost/compress v1.18.4 // indirect
	github.com/mattn/go-sqlite3 v1.14.34 // indirect
	github.com/nats-io/nats.go v1.49.0 // indirect
	github.com/nats-io/nkeys v0.4.15 // indirect
	github.com/nats-io/nuid v1.0.1 // indirect
	github.com/parquet-go/bitpack v1.0.0 // indirect
	github.com/parquet-go/jsonlite v1.4.0 // indirect
	github.com/parquet-go/parquet-go v0.28.0 // indirect
	github.com/pierrec/lz4/v4 v4.1.26 // indirect
	github.com/swaggo/files v1.0.1 // indirect
	github.com/twpayne/go-geom v1.6.1 // indirect
	go.yaml.in/yaml/v3 v3.0.4 // indirect
	golang.org/x/crypto v0.48.0 // indirect
	golang.org/x/mod v0.33.0 // indirect
	golang.org/x/net v0.51.0 // indirect
	golang.org/x/sync v0.19.0 // indirect
	golang.org/x/sys v0.41.0 // indirect
	golang.org/x/tools v0.42.0 // indirect
	google.golang.org/protobuf v1.36.11 // indirect
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)


     1→TARGET = ./cc-metric-store
     2→VAR = ./var/checkpoints/
     3→VERSION = 1.5.0
     4→GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
     5→CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
     6→LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'
     7→
     8→.PHONY: clean distclean test swagger $(TARGET)
     9→
    10→.NOTPARALLEL:
    11→
    12→$(TARGET): config.json $(VAR)
    13→	$(info ===>  BUILD cc-metric-store)
    14→	@go build -ldflags=${LD_FLAGS} ./cmd/cc-metric-store
    15→
    16→config.json:
    17→	@cp ./configs/config.json config.json
    18→
    19→$(VAR):
    20→	@mkdir -p $(VAR)
    21→
    22→swagger:
    23→	$(info ===>  GENERATE swagger)
    24→	@go run github.com/swaggo/swag/cmd/swag init -d ./internal/api,./internal/util -g api.go -o ./api
    25→	@mv ./api/docs.go ./internal/api/docs.go
    26→
    27→clean:
    28→	$(info ===>  CLEAN)
    29→	@go clean
    30→	@rm -f $(TARGET)
    31→
    32→distclean: clean
    33→	@rm -rf ./var
    34→	@rm -f config.json
    35→
    36→test:
    37→	$(info ===>  TESTING)
    38→	@go clean -testcache
    39→	@go build ./...
    40→	@go vet ./...
    41→	@go test ./...
    42→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

TARGET = ./cc-metric-store
VAR = ./var/checkpoints/
VERSION = 1.5.0
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'

.PHONY: clean distclean test swagger $(TARGET)

.NOTPARALLEL:

$(TARGET): config.json $(VAR)
	$(info ===>  BUILD cc-metric-store)
	@go build -ldflags=${LD_FLAGS} ./cmd/cc-metric-store

config.json:
	@cp ./configs/config.json config.json

$(VAR):
	@mkdir -p $(VAR)

swagger:
	$(info ===>  GENERATE swagger)
	@go run github.com/swaggo/swag/cmd/swag init -d ./internal/api,./internal/util -g api.go -o ./api
	@mv ./api/docs.go ./internal/api/docs.go

clean:
	$(info ===>  CLEAN)
	@go clean
	@rm -f $(TARGET)

distclean: clean
	@rm -rf ./var
	@rm -f config.json

test:
	$(info ===>  TESTING)
	@go clean -testcache
	@go build ./...
	@go vet ./...
	@go test ./...


Let me look at the current swagger integration to understand what's already there and what needs to be updated.


     1→// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
     2→// All rights reserved. This file is part of cc-metric-store.
     3→// Use of this source code is governed by a MIT-style
     4→// license that can be found in the LICENSE file.
     5→
     6→package api
     7→
     8→import (
     9→	"crypto/ed25519"
    10→	"encoding/base64"
    11→	"log"
    12→	"net/http"
    13→
    14→	"github.com/ClusterCockpit/cc-metric-store/internal/config"
    15→)
    16→
    17→func MountRoutes(r *http.ServeMux) {
    18→	if len(config.Keys.JwtPublicKey) > 0 {
    19→		buf, err := base64.StdEncoding.DecodeString(config.Keys.JwtPublicKey)
    20→		if err != nil {
    21→			log.Fatalf("starting server failed: %v", err)
    22→		}
    23→		publicKey := ed25519.PublicKey(buf)
    24→		// Compatibility
    25→		r.Handle("POST /api/free", authHandler(http.HandlerFunc(freeMetrics), publicKey))
    26→		r.Handle("POST /api/write", authHandler(http.HandlerFunc(writeMetrics), publicKey))
    27→		r.Handle("GET /api/query", authHandler(http.HandlerFunc(handleQuery), publicKey))
    28→		r.Handle("GET /api/debug", authHandler(http.HandlerFunc(debugMetrics), publicKey))
    29→		r.Handle("GET /api/healthcheck", authHandler(http.HandlerFunc(metricsHealth), publicKey))
    30→		// Refactor
    31→		r.Handle("POST /api/free/", authHandler(http.HandlerFunc(freeMetrics), publicKey))
    32→		r.Handle("POST /api/write/", authHandler(http.HandlerFunc(writeMetrics), publicKey))
    33→		r.Handle("GET /api/query/", authHandler(http.HandlerFunc(handleQuery), publicKey))
    34→		r.Handle("GET /api/debug/", authHandler(http.HandlerFunc(debugMetrics), publicKey))
    35→		r.Handle("GET /api/healthcheck/", authHandler(http.HandlerFunc(metricsHealth), publicKey))
    36→	} else {
    37→		// Compatibility
    38→		r.HandleFunc("POST /api/free", freeMetrics)
    39→		r.HandleFunc("POST /api/write", writeMetrics)
    40→		r.HandleFunc("GET /api/query", handleQuery)
    41→		r.HandleFunc("GET /api/debug", debugMetrics)
    42→		r.HandleFunc("GET /api/healthcheck", metricsHealth)
    43→		// Refactor
    44→		r.HandleFunc("POST /api/free/", freeMetrics)
    45→		r.HandleFunc("POST /api/write/", writeMetrics)
    46→		r.HandleFunc("GET /api/query/", handleQuery)
    47→		r.HandleFunc("GET /api/debug/", debugMetrics)
    48→		r.HandleFunc("GET /api/healthcheck/", metricsHealth)
    49→	}
    50→}
    51→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-metric-store.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package api

import (
	"crypto/ed25519"
	"encoding/base64"
	"log"
	"net/http"

	"github.com/ClusterCockpit/cc-metric-store/internal/config"
)

func MountRoutes(r *http.ServeMux) {
	if len(config.Keys.JwtPublicKey) > 0 {
		buf, err := base64.StdEncoding.DecodeString(config.Keys.JwtPublicKey)
		if err != nil {
			log.Fatalf("starting server failed: %v", err)
		}
		publicKey := ed25519.PublicKey(buf)
		// Compatibility
		r.Handle("POST /api/free", authHandler(http.HandlerFunc(freeMetrics), publicKey))
		r.Handle("POST /api/write", authHandler(http.HandlerFunc(writeMetrics), publicKey))
		r.Handle("GET /api/query", authHandler(http.HandlerFunc(handleQuery), publicKey))
		r.Handle("GET /api/debug", authHandler(http.HandlerFunc(debugMetrics), publicKey))
		r.Handle("GET /api/healthcheck", authHandler(http.HandlerFunc(metricsHealth), publicKey))
		// Refactor
		r.Handle("POST /api/free/", authHandler(http.HandlerFunc(freeMetrics), publicKey))
		r.Handle("POST /api/write/", authHandler(http.HandlerFunc(writeMetrics), publicKey))
		r.Handle("GET /api/query/", authHandler(http.HandlerFunc(handleQuery), publicKey))
		r.Handle("GET /api/debug/", authHandler(http.HandlerFunc(debugMetrics), publicKey))
		r.Handle("GET /api/healthcheck/", authHandler(http.HandlerFunc(metricsHealth), publicKey))
	} else {
		// Compatibility
		r.HandleFunc("POST /api/free", freeMetrics)
		r.HandleFunc("POST /api/write", writeMetrics)
		r.HandleFunc("GET /api/query", handleQuery)
		r.HandleFunc("GET /api/debug", debugMetrics)
		r.HandleFunc("GET /api/healthcheck", metricsHealth)
		// Refactor
		r.HandleFunc("POST /api/free/", freeMetrics)
		r.HandleFunc("POST /api/write/", writeMetrics)
		r.HandleFunc("GET /api/query/", handleQuery)
		r.HandleFunc("GET /api/debug/", debugMetrics)
		r.HandleFunc("GET /api/healthcheck/", metricsHealth)
	}
}


     1→// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
     2→// All rights reserved. This file is part of cc-metric-store.
     3→// Use of this source code is governed by a MIT-style
     4→// license that can be found in the LICENSE file.
     5→
     6→// Package api Code generated by swaggo/swag. DO NOT EDIT
     7→package api
     8→
     9→import "github.com/swaggo/swag"
    10→
    11→const docTemplate = `{
    12→    "schemes": {{ marshal .Schemes }},
    13→    "swagger": "2.0",
    14→    "info": {
    15→        "description": "{{escape .Description}}",
    16→        "title": "{{.Title}}",
    17→        "contact": {
    18→            "name": "ClusterCockpit Project",
    19→            "url": "https://clustercockpit.org",
    20→            "email": "support@clustercockpit.org"
    21→        },
    22→        "license": {
    23→            "name": "MIT License",
    24→            "url": "https://opensource.org/licenses/MIT"
    25→        },
    26→        "version": "{{.Version}}"
    27→    },
    28→    "host": "{{.Host}}",
    29→    "basePath": "{{.BasePath}}",
    30→    "paths": {
    31→        "/debug/": {
    32→            "post": {
    33→                "security": [
    34→                    {
    35→                        "ApiKeyAuth": []
    36→                    }
    37→                ],
    38→                "description": "This endpoint allows the users to print the content of",
    39→                "produces": [
    40→                    "application/json"
    41→                ],
    42→                "tags": [
    43→                    "debug"
    44→                ],
    45→                "summary": "Debug endpoint",
    46→                "parameters": [
    47→                    {
    48→                        "type": "string",
    49→                        "description": "Selector",
    50→                        "name": "selector",
    51→                        "in": "query"
    52→                    }
    53→                ],
    54→                "responses": {
    55→                    "200": {
    56→                        "description": "Debug dump",
    57→                        "schema": {
    58→                            "type": "string"
    59→                        }
    60→                    },
    61→                    "400": {
    62→                        "description": "Bad Request",
    63→                        "schema": {
    64→                            "$ref": "#/definitions/api.ErrorResponse"
    65→                        }
    66→                    },
    67→                    "401": {
    68→                        "description": "Unauthorized",
    69→                        "schema": {
    70→                            "$ref": "#/definitions/api.ErrorResponse"
    71→                        }
    72→                    },
    73→                    "403": {
    74→                        "description": "Forbidden",
    75→                        "schema": {
    76→                            "$ref": "#/definitions/api.ErrorResponse"
    77→                        }
    78→                    },
    79→                    "500": {
    80→                        "description": "Internal Server Error",
    81→                        "schema": {
    82→                            "$ref": "#/definitions/api.ErrorResponse"
    83→                        }
    84→                    }
    85→                }
    86→            }
    87→        },
    88→        "/free/": {
    89→            "post": {
    90→                "security": [
    91→                    {
    92→                        "ApiKeyAuth": []
    93→                    }
    94→                ],
    95→                "description": "This endpoint allows the users to free the Buffers from the",
    96→                "produces": [
    97→                    "application/json"
    98→                ],
    99→                "tags": [
   100→                    "free"
   101→                ],
   102→                "parameters": [
   103→                    {
   104→                        "type": "string",
   105→                        "description": "up to timestamp",
   106→                        "name": "to",
   107→                        "in": "query"
   108→                    }
   109→                ],
   110→                "responses": {
   111→                    "200": {
   112→                        "description": "ok",
   113→                        "schema": {
   114→                            "type": "string"
   115→                        }
   116→                    },
   117→                    "400": {
   118→                        "description": "Bad Request",
   119→                        "schema": {
   120→                            "$ref": "#/definitions/api.ErrorResponse"
   121→                        }
   122→                    },
   123→                    "401": {
   124→                        "description": "Unauthorized",
   125→                        "schema": {
   126→                            "$ref": "#/definitions/api.ErrorResponse"
   127→                        }
   128→                    },
   129→                    "403": {
   130→                        "description": "Forbidden",
   131→                        "schema": {
   132→                            "$ref": "#/definitions/api.ErrorResponse"
   133→                        }
   134→                    },
   135→                    "500": {
   136→                        "description": "Internal Server Error",
   137→                        "schema": {
   138→                            "$ref": "#/definitions/api.ErrorResponse"
   139→                        }
   140→                    }
   141→                }
   142→            }
   143→        },
   144→        "/healthcheck/": {
   145→            "get": {
   146→                "security": [
   147→                    {
   148→                        "ApiKeyAuth": []
   149→                    }
   150→                ],
   151→                "description": "This endpoint allows the users to check if a node is healthy",
   152→                "produces": [
   153→                    "application/json"
   154→                ],
   155→                "tags": [
   156→                    "healthcheck"
   157→                ],
   158→                "summary": "HealthCheck endpoint",
   159→                "parameters": [
   160→                    {
   161→                        "type": "string",
   162→                        "description": "Selector",
   163→                        "name": "selector",
   164→                        "in": "query"
   165→                    }
   166→                ],
   167→                "responses": {
   168→                    "200": {
   169→                        "description": "Debug dump",
   170→                        "schema": {
   171→                            "type": "string"
   172→                        }
   173→                    },
   174→                    "400": {
   175→                        "description": "Bad Request",
   176→                        "schema": {
   177→                            "$ref": "#/definitions/api.ErrorResponse"
   178→                        }
   179→                    },
   180→                    "401": {
   181→                        "description": "Unauthorized",
   182→                        "schema": {
   183→                            "$ref": "#/definitions/api.ErrorResponse"
   184→                        }
   185→                    },
   186→                    "403": {
   187→                        "description": "Forbidden",
   188→                        "schema": {
   189→                            "$ref": "#/definitions/api.ErrorResponse"
   190→                        }
   191→                    },
   192→                    "500": {
   193→                        "description": "Internal Server Error",
   194→                        "schema": {
   195→                            "$ref": "#/definitions/api.ErrorResponse"
   196→                        }
   197→                    }
   198→                }
   199→            }
   200→        },
   201→        "/query/": {
   202→            "get": {
   203→                "security": [
   204→                    {
   205→                        "ApiKeyAuth": []
   206→                    }
   207→                ],
   208→                "description": "This endpoint allows the users to retrieve data from the",
   209→                "consumes": [
   210→                    "application/json"
   211→                ],
   212→                "produces": [
   213→                    "application/json"
   214→                ],
   215→                "tags": [
   216→                    "query"
   217→                ],
   218→                "summary": "Query metrics",
   219→                "parameters": [
   220→                    {
   221→                        "description": "API query payload object",
   222→                        "name": "request",
   223→                        "in": "body",
   224→                        "required": true,
   225→                        "schema": {
   226→                            "$ref": "#/definitions/api.ApiQueryRequest"
   227→                        }
   228→                    }
   229→                ],
   230→                "responses": {
   231→                    "200": {
   232→                        "description": "API query response object",
   233→                        "schema": {
   234→                            "$ref": "#/definitions/api.ApiQueryResponse"
   235→                        }
   236→                    },
   237→                    "400": {
   238→                        "description": "Bad Request",
   239→                        "schema": {
   240→                            "$ref": "#/definitions/api.ErrorResponse"
   241→                        }
   242→                    },
   243→                    "401": {
   244→                        "description": "Unauthorized",
   245→                        "schema": {
   246→                            "$ref": "#/definitions/api.ErrorResponse"
   247→                        }
   248→                    },
   249→                    "403": {
   250→                        "description": "Forbidden",
   251→                        "schema": {
   252→                            "$ref": "#/definitions/api.ErrorResponse"
   253→                        }
   254→                    },
   255→                    "500": {
   256→                        "description": "Internal Server Error",
   257→                        "schema": {
   258→                            "$ref": "#/definitions/api.ErrorResponse"
   259→                        }
   260→                    }
   261→                }
   262→            }
   263→        },
   264→        "/write/": {
   265→            "post": {
   266→                "security": [
   267→                    {
   268→                        "ApiKeyAuth": []
   269→                    }
   270→                ],
   271→                "consumes": [
   272→                    "text/plain"
   273→                ],
   274→                "produces": [
   275→                    "application/json"
   276→                ],
   277→                "parameters": [
   278→                    {
   279→                        "type": "string",
   280→                        "description": "If the lines in the body do not have a cluster tag, use this value instead.",
   281→                        "name": "cluster",
   282→                        "in": "query"
   283→                    }
   284→                ],
   285→                "responses": {
   286→                    "200": {
   287→                        "description": "ok",
   288→                        "schema": {
   289→                            "type": "string"
   290→                        }
   291→                    },
   292→                    "400": {
   293→                        "description": "Bad Request",
   294→                        "schema": {
   295→                            "$ref": "#/definitions/api.ErrorResponse"
   296→                        }
   297→                    },
   298→                    "401": {
   299→                        "description": "Unauthorized",
   300→                        "schema": {
   301→                            "$ref": "#/definitions/api.ErrorResponse"
   302→                        }
   303→                    },
   304→                    "403": {
   305→                        "description": "Forbidden",
   306→                        "schema": {
   307→                            "$ref": "#/definitions/api.ErrorResponse"
   308→                        }
   309→                    },
   310→                    "500": {
   311→                        "description": "Internal Server Error",
   312→                        "schema": {
   313→                            "$ref": "#/definitions/api.ErrorResponse"
   314→                        }
   315→                    }
   316→                }
   317→            }
   318→        }
   319→    },
   320→    "definitions": {
   321→        "api.ApiMetricData": {
   322→            "type": "object",
   323→            "properties": {
   324→                "avg": {
   325→                    "type": "number"
   326→                },
   327→                "data": {
   328→                    "type": "array",
   329→                    "items": {
   330→                        "type": "number"
   331→                    }
   332→                },
   333→                "error": {
   334→                    "type": "string"
   335→                },
   336→                "from": {
   337→                    "type": "integer"
   338→                },
   339→                "max": {
   340→                    "type": "number"
   341→                },
   342→                "min": {
   343→                    "type": "number"
   344→                },
   345→                "resolution": {
   346→                    "type": "integer"
   347→                },
   348→                "to": {
   349→                    "type": "integer"
   350→                }
   351→            }
   352→        },
   353→        "api.ApiQuery": {
   354→            "type": "object",
   355→            "properties": {
   356→                "aggreg": {
   357→                    "type": "boolean"
   358→                },
   359→                "host": {
   360→                    "type": "string"
   361→                },
   362→                "metric": {
   363→                    "type": "string"
   364→                },
   365→                "resolution": {
   366→                    "type": "integer"
   367→                },
   368→                "scale-by": {
   369→                    "type": "number"
   370→                },
   371→                "subtype": {
   372→                    "type": "string"
   373→                },
   374→                "subtype-ids": {
   375→                    "type": "array",
   376→                    "items": {
   377→                        "type": "string"
   378→                    }
   379→                },
   380→                "type": {
   381→                    "type": "string"
   382→                },
   383→                "type-ids": {
   384→                    "type": "array",
   385→                    "items": {
   386→                        "type": "string"
   387→                    }
   388→                }
   389→            }
   390→        },
   391→        "api.ApiQueryRequest": {
   392→            "type": "object",
   393→            "properties": {
   394→                "cluster": {
   395→                    "type": "string"
   396→                },
   397→                "for-all-nodes": {
   398→                    "type": "array",
   399→                    "items": {
   400→                        "type": "string"
   401→                    }
   402→                },
   403→                "from": {
   404→                    "type": "integer"
   405→                },
   406→                "queries": {
   407→                    "type": "array",
   408→                    "items": {
   409→                        "$ref": "#/definitions/api.ApiQuery"
   410→                    }
   411→                },
   412→                "to": {
   413→                    "type": "integer"
   414→                },
   415→                "with-data": {
   416→                    "type": "boolean"
   417→                },
   418→                "with-padding": {
   419→                    "type": "boolean"
   420→                },
   421→                "with-stats": {
   422→                    "type": "boolean"
   423→                }
   424→            }
   425→        },
   426→        "api.ApiQueryResponse": {
   427→            "type": "object",
   428→            "properties": {
   429→                "queries": {
   430→                    "type": "array",
   431→                    "items": {
   432→                        "$ref": "#/definitions/api.ApiQuery"
   433→                    }
   434→                },
   435→                "results": {
   436→                    "type": "array",
   437→                    "items": {
   438→                        "type": "array",
   439→                        "items": {
   440→                            "$ref": "#/definitions/api.ApiMetricData"
   441→                        }
   442→                    }
   443→                }
   444→            }
   445→        },
   446→        "api.ErrorResponse": {
   447→            "type": "object",
   448→            "properties": {
   449→                "error": {
   450→                    "description": "Error Message",
   451→                    "type": "string"
   452→                },
   453→                "status": {
   454→                    "description": "Statustext of Errorcode",
   455→                    "type": "string"
   456→                }
   457→            }
   458→        }
   459→    },
   460→    "securityDefinitions": {
   461→        "ApiKeyAuth": {
   462→            "type": "apiKey",
   463→            "name": "X-Auth-Token",
   464→            "in": "header"
   465→        }
   466→    }
   467→}`
   468→
   469→// SwaggerInfo holds exported Swagger Info so clients can modify it
   470→var SwaggerInfo = &swag.Spec{
   471→	Version:          "1.0.0",
   472→	Host:             "localhost:8082",
   473→	BasePath:         "/api/",
   474→	Schemes:          []string{},
   475→	Title:            "cc-metric-store REST API",
   476→	Description:      "API for cc-metric-store",
   477→	InfoInstanceName: "swagger",
   478→	SwaggerTemplate:  docTemplate,
   479→	LeftDelim:        "{{",
   480→	RightDelim:       "}}",
   481→}
   482→
   483→func init() {
   484→	swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
   485→}
   486→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-metric-store.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// Package api Code generated by swaggo/swag. DO NOT EDIT
package api

import "github.com/swaggo/swag"

const docTemplate = `{
    "schemes": {{ marshal .Schemes }},
    "swagger": "2.0",
    "info": {
        "description": "{{escape .Description}}",
        "title": "{{.Title}}",
        "contact": {
            "name": "ClusterCockpit Project",
            "url": "https://clustercockpit.org",
            "email": "support@clustercockpit.org"
        },
        "license": {
            "name": "MIT License",
            "url": "https://opensource.org/licenses/MIT"
        },
        "version": "{{.Version}}"
    },
    "host": "{{.Host}}",
    "basePath": "{{.BasePath}}",
    "paths": {
        "/debug/": {
            "post": {
                "security": [
                    {
                        "ApiKeyAuth": []
                    }
                ],
                "description": "This endpoint allows the users to print the content of",
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "debug"
                ],
                "summary": "Debug endpoint",
                "parameters": [
                    {
                        "type": "string",
                        "description": "Selector",
                        "name": "selector",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Debug dump",
                        "schema": {
                            "type": "string"
                        }
                    },
                    "400": {
                        "description": "Bad Request",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "401": {
                        "description": "Unauthorized",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "403": {
                        "description": "Forbidden",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "500": {
                        "description": "Internal Server Error",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    }
                }
            }
        },
        "/free/": {
            "post": {
                "security": [
                    {
                        "ApiKeyAuth": []
                    }
                ],
                "description": "This endpoint allows the users to free the Buffers from the",
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "free"
                ],
                "parameters": [
                    {
                        "type": "string",
                        "description": "up to timestamp",
                        "name": "to",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "ok",
                        "schema": {
                            "type": "string"
                        }
                    },
                    "400": {
                        "description": "Bad Request",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "401": {
                        "description": "Unauthorized",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "403": {
                        "description": "Forbidden",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "500": {
                        "description": "Internal Server Error",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    }
                }
            }
        },
        "/healthcheck/": {
            "get": {
                "security": [
                    {
                        "ApiKeyAuth": []
                    }
                ],
                "description": "This endpoint allows the users to check if a node is healthy",
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "healthcheck"
                ],
                "summary": "HealthCheck endpoint",
                "parameters": [
                    {
                        "type": "string",
                        "description": "Selector",
                        "name": "selector",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Debug dump",
                        "schema": {
                            "type": "string"
                        }
                    },
                    "400": {
                        "description": "Bad Request",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "401": {
                        "description": "Unauthorized",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "403": {
                        "description": "Forbidden",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "500": {
                        "description": "Internal Server Error",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    }
                }
            }
        },
        "/query/": {
            "get": {
                "security": [
                    {
                        "ApiKeyAuth": []
                    }
                ],
                "description": "This endpoint allows the users to retrieve data from the",
                "consumes": [
                    "application/json"
                ],
                "produces": [
                    "application/json"
                ],
                "tags": [
                    "query"
                ],
                "summary": "Query metrics",
                "parameters": [
                    {
                        "description": "API query payload object",
                        "name": "request",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/api.ApiQueryRequest"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "API query response object",
                        "schema": {
                            "$ref": "#/definitions/api.ApiQueryResponse"
                        }
                    },
                    "400": {
                        "description": "Bad Request",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "401": {
                        "description": "Unauthorized",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "403": {
                        "description": "Forbidden",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "500": {
                        "description": "Internal Server Error",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    }
                }
            }
        },
        "/write/": {
            "post": {
                "security": [
                    {
                        "ApiKeyAuth": []
                    }
                ],
                "consumes": [
                    "text/plain"
                ],
                "produces": [
                    "application/json"
                ],
                "parameters": [
                    {
                        "type": "string",
                        "description": "If the lines in the body do not have a cluster tag, use this value instead.",
                        "name": "cluster",
                        "in": "query"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "ok",
                        "schema": {
                            "type": "string"
                        }
                    },
                    "400": {
                        "description": "Bad Request",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "401": {
                        "description": "Unauthorized",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "403": {
                        "description": "Forbidden",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    },
                    "500": {
                        "description": "Internal Server Error",
                        "schema": {
                            "$ref": "#/definitions/api.ErrorResponse"
                        }
                    }
                }
            }
        }
    },
    "definitions": {
        "api.ApiMetricData": {
            "type": "object",
            "properties": {
                "avg": {
                    "type": "number"
                },
                "data": {
                    "type": "array",
                    "items": {
                        "type": "number"
                    }
                },
                "error": {
                    "type": "string"
                },
                "from": {
                    "type": "integer"
                },
                "max": {
                    "type": "number"
                },
                "min": {
                    "type": "number"
                },
                "resolution": {
                    "type": "integer"
                },
                "to": {
                    "type": "integer"
                }
            }
        },
        "api.ApiQuery": {
            "type": "object",
            "properties": {
                "aggreg": {
                    "type": "boolean"
                },
                "host": {
                    "type": "string"
                },
                "metric": {
                    "type": "string"
                },
                "resolution": {
                    "type": "integer"
                },
                "scale-by": {
                    "type": "number"
                },
                "subtype": {
                    "type": "string"
                },
                "subtype-ids": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                },
                "type": {
                    "type": "string"
                },
                "type-ids": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                }
            }
        },
        "api.ApiQueryRequest": {
            "type": "object",
            "properties": {
                "cluster": {
                    "type": "string"
                },
                "for-all-nodes": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                },
                "from": {
                    "type": "integer"
                },
                "queries": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/api.ApiQuery"
                    }
                },
                "to": {
                    "type": "integer"
                },
                "with-data": {
                    "type": "boolean"
                },
                "with-padding": {
                    "type": "boolean"
                },
                "with-stats": {
                    "type": "boolean"
                }
            }
        },
        "api.ApiQueryResponse": {
            "type": "object",
            "properties": {
                "queries": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/api.ApiQuery"
                    }
                },
                "results": {
                    "type": "array",
                    "items": {
                        "type": "array",
                        "items": {
                            "$ref": "#/definitions/api.ApiMetricData"
                        }
                    }
                }
            }
        },
        "api.ErrorResponse": {
            "type": "object",
            "properties": {
                "error": {
                    "description": "Error Message",
                    "type": "string"
                },
                "status": {
                    "description": "Statustext of Errorcode",
                    "type": "string"
                }
            }
        }
    },
    "securityDefinitions": {
        "ApiKeyAuth": {
            "type": "apiKey",
            "name": "X-Auth-Token",
            "in": "header"
        }
    }
}`

// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
	Version:          "1.0.0",
	Host:             "localhost:8082",
	BasePath:         "/api/",
	Schemes:          []string{},
	Title:            "cc-metric-store REST API",
	Description:      "API for cc-metric-store",
	InfoInstanceName: "swagger",
	SwaggerTemplate:  docTemplate,
	LeftDelim:        "{{",
	RightDelim:       "}}",
}

func init() {
	swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}


     1→// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
     2→// All rights reserved. This file is part of cc-metric-store.
     3→// Use of this source code is governed by a MIT-style
     4→// license that can be found in the LICENSE file.
     5→
     6→// Package main provides the entry point for the ClusterCockpit metric store server.
     7→// This file contains HTTP server setup, routing configuration.
     8→package main
     9→
    10→import (
    11→	"context"
    12→	"crypto/tls"
    13→	"fmt"
    14→	"net"
    15→	"net/http"
    16→	"time"
    17→
    18→	"github.com/ClusterCockpit/cc-backend/pkg/metricstore"
    19→	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
    20→	"github.com/ClusterCockpit/cc-lib/v2/nats"
    21→	"github.com/ClusterCockpit/cc-lib/v2/runtime"
    22→	"github.com/ClusterCockpit/cc-metric-store/internal/api"
    23→	"github.com/ClusterCockpit/cc-metric-store/internal/config"
    24→	httpSwagger "github.com/swaggo/http-swagger"
    25→)
    26→
    27→// Server encapsulates the HTTP server state and dependencies
    28→type Server struct {
    29→	router *http.ServeMux
    30→	server *http.Server
    31→}
    32→
    33→// NewServer creates and initializes a new Server instance
    34→func NewServer(version, commit, buildDate string) (*Server, error) {
    35→	s := &Server{
    36→		router: http.NewServeMux(),
    37→	}
    38→
    39→	if err := s.init(); err != nil {
    40→		return nil, err
    41→	}
    42→
    43→	return s, nil
    44→}
    45→
    46→func (s *Server) init() error {
    47→	api.MountRoutes(s.router)
    48→
    49→	if flagDev {
    50→		cclog.Print("Enable Swagger UI!")
    51→		s.router.HandleFunc("GET /swagger/", httpSwagger.Handler(
    52→			httpSwagger.URL("http://"+config.Keys.Address+"/swagger/doc.json")))
    53→	}
    54→
    55→	return nil
    56→}
    57→
    58→// Server timeout defaults (in seconds)
    59→const (
    60→	defaultReadTimeout  = 30
    61→	defaultWriteTimeout = 30
    62→)
    63→
    64→func (s *Server) Start(ctx context.Context) error {
    65→	// Use configurable timeouts with defaults
    66→	readTimeout := time.Duration(defaultReadTimeout) * time.Second
    67→	writeTimeout := time.Duration(defaultWriteTimeout) * time.Second
    68→
    69→	s.server = &http.Server{
    70→		ReadTimeout:  readTimeout,
    71→		WriteTimeout: writeTimeout,
    72→		Handler:      s.router,
    73→		Addr:         config.Keys.Address,
    74→	}
    75→
    76→	// Start http or https server
    77→	listener, err := net.Listen("tcp", config.Keys.Address)
    78→	if err != nil {
    79→		return fmt.Errorf("starting listener on '%s': %w", config.Keys.Address, err)
    80→	}
    81→
    82→	if config.Keys.CertFile != "" && config.Keys.KeyFile != "" {
    83→		cert, err := tls.LoadX509KeyPair(
    84→			config.Keys.CertFile, config.Keys.KeyFile)
    85→		if err != nil {
    86→			return fmt.Errorf("loading X509 keypair (check 'https-cert-file' and 'https-key-file' in config.json): %w", err)
    87→		}
    88→		listener = tls.NewListener(listener, &tls.Config{
    89→			Certificates: []tls.Certificate{cert},
    90→			CipherSuites: []uint16{
    91→				tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
    92→				tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
    93→			},
    94→			MinVersion:               tls.VersionTLS12,
    95→			PreferServerCipherSuites: true,
    96→		})
    97→		cclog.Infof("HTTPS server listening at %s...", config.Keys.Address)
    98→	} else {
    99→		cclog.Infof("HTTP server listening at %s...", config.Keys.Address)
   100→	}
   101→
   102→	// Because this program will want to bind to a privileged port (like 80), the listener must
   103→	// be established first, then the user can be changed, and after that,
   104→	// the actual http server can be started.
   105→	if err := runtime.DropPrivileges(config.Keys.Group, config.Keys.User); err != nil {
   106→		return fmt.Errorf("dropping privileges: %w", err)
   107→	}
   108→
   109→	// Handle context cancellation for graceful shutdown
   110→	go func() {
   111→		<-ctx.Done()
   112→		shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
   113→		defer cancel()
   114→		if err := s.server.Shutdown(shutdownCtx); err != nil {
   115→			cclog.Errorf("Server shutdown error: %v", err)
   116→		}
   117→	}()
   118→
   119→	if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
   120→		return fmt.Errorf("server failed: %w", err)
   121→	}
   122→	return nil
   123→}
   124→
   125→func (s *Server) Shutdown(ctx context.Context) {
   126→	// Create a shutdown context with timeout
   127→	shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
   128→	defer cancel()
   129→
   130→	nc := nats.GetClient()
   131→	if nc != nil {
   132→		nc.Close()
   133→	}
   134→
   135→	// First shut down the server gracefully (waiting for all ongoing requests)
   136→	if err := s.server.Shutdown(shutdownCtx); err != nil {
   137→		cclog.Errorf("Server shutdown error: %v", err)
   138→	}
   139→
   140→	// Archive all the metric store data
   141→	metricstore.Shutdown()
   142→}
   143→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-metric-store.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// Package main provides the entry point for the ClusterCockpit metric store server.
// This file contains HTTP server setup, routing configuration.
package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"time"

	"github.com/ClusterCockpit/cc-backend/pkg/metricstore"
	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
	"github.com/ClusterCockpit/cc-lib/v2/nats"
	"github.com/ClusterCockpit/cc-lib/v2/runtime"
	"github.com/ClusterCockpit/cc-metric-store/internal/api"
	"github.com/ClusterCockpit/cc-metric-store/internal/config"
	httpSwagger "github.com/swaggo/http-swagger"
)

// Server encapsulates the HTTP server state and dependencies
type Server struct {
	router *http.ServeMux
	server *http.Server
}

// NewServer creates and initializes a new Server instance
func NewServer(version, commit, buildDate string) (*Server, error) {
	s := &Server{
		router: http.NewServeMux(),
	}

	if err := s.init(); err != nil {
		return nil, err
	}

	return s, nil
}

func (s *Server) init() error {
	api.MountRoutes(s.router)

	if flagDev {
		cclog.Print("Enable Swagger UI!")
		s.router.HandleFunc("GET /swagger/", httpSwagger.Handler(
			httpSwagger.URL("http://"+config.Keys.Address+"/swagger/doc.json")))
	}

	return nil
}

// Server timeout defaults (in seconds)
const (
	defaultReadTimeout  = 30
	defaultWriteTimeout = 30
)

func (s *Server) Start(ctx context.Context) error {
	// Use configurable timeouts with defaults
	readTimeout := time.Duration(defaultReadTimeout) * time.Second
	writeTimeout := time.Duration(defaultWriteTimeout) * time.Second

	s.server = &http.Server{
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
		Handler:      s.router,
		Addr:         config.Keys.Address,
	}

	// Start http or https server
	listener, err := net.Listen("tcp", config.Keys.Address)
	if err != nil {
		return fmt.Errorf("starting listener on '%s': %w", config.Keys.Address, err)
	}

	if config.Keys.CertFile != "" && config.Keys.KeyFile != "" {
		cert, err := tls.LoadX509KeyPair(
			config.Keys.CertFile, config.Keys.KeyFile)
		if err != nil {
			return fmt.Errorf("loading X509 keypair (check 'https-cert-file' and 'https-key-file' in config.json): %w", err)
		}
		listener = tls.NewListener(listener, &tls.Config{
			Certificates: []tls.Certificate{cert},
			CipherSuites: []uint16{
				tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
				tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
			},
			MinVersion:               tls.VersionTLS12,
			PreferServerCipherSuites: true,
		})
		cclog.Infof("HTTPS server listening at %s...", config.Keys.Address)
	} else {
		cclog.Infof("HTTP server listening at %s...", config.Keys.Address)
	}

	// Because this program will want to bind to a privileged port (like 80), the listener must
	// be established first, then the user can be changed, and after that,
	// the actual http server can be started.
	if err := runtime.DropPrivileges(config.Keys.Group, config.Keys.User); err != nil {
		return fmt.Errorf("dropping privileges: %w", err)
	}

	// Handle context cancellation for graceful shutdown
	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()
		if err := s.server.Shutdown(shutdownCtx); err != nil {
			cclog.Errorf("Server shutdown error: %v", err)
		}
	}()

	if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
		return fmt.Errorf("server failed: %w", err)
	}
	return nil
}

func (s *Server) Shutdown(ctx context.Context) {
	// Create a shutdown context with timeout
	shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	nc := nats.GetClient()
	if nc != nil {
		nc.Close()
	}

	// First shut down the server gracefully (waiting for all ongoing requests)
	if err := s.server.Shutdown(shutdownCtx); err != nil {
		cclog.Errorf("Server shutdown error: %v", err)
	}

	// Archive all the metric store data
	metricstore.Shutdown()
}


     1→// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
     2→// All rights reserved. This file is part of cc-metric-store.
     3→// Use of this source code is governed by a MIT-style
     4→// license that can be found in the LICENSE file.
     5→
     6→package api
     7→
     8→import (
     9→	"bufio"
    10→	"encoding/json"
    11→	"errors"
    12→	"fmt"
    13→	"log"
    14→	"math"
    15→	"net/http"
    16→	"strconv"
    17→	"strings"
    18→
    19→	"github.com/ClusterCockpit/cc-backend/pkg/metricstore"
    20→	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
    21→	"github.com/ClusterCockpit/cc-lib/v2/schema"
    22→	"github.com/ClusterCockpit/cc-lib/v2/util"
    23→	"github.com/ClusterCockpit/cc-line-protocol/v2/lineprotocol"
    24→)
    25→
    26→// ErrorResponse model
    27→type ErrorResponse struct {
    28→	// Statustext of Errorcode
    29→	Status string `json:"status"`
    30→	Error  string `json:"error"` // Error Message
    31→}
    32→
    33→// DefaultAPIResponse model
    34→type DefaultAPIResponse struct {
    35→	Message string `json:"msg"`
    36→}
    37→
    38→// handleError writes a standardized JSON error response with the given status code.
    39→// It logs the error at WARN level and ensures proper Content-Type headers are set.
    40→func handleError(err error, statusCode int, rw http.ResponseWriter) {
    41→	cclog.Warnf("REST ERROR : %s", err.Error())
    42→	rw.Header().Add("Content-Type", "application/json")
    43→	rw.WriteHeader(statusCode)
    44→	if err := json.NewEncoder(rw).Encode(ErrorResponse{
    45→		Status: http.StatusText(statusCode),
    46→		Error:  err.Error(),
    47→	}); err != nil {
    48→		cclog.Errorf("Failed to encode error response: %v", err)
    49→	}
    50→}
    51→
    52→type APIMetricData struct {
    53→	Error      *string           `json:"error,omitempty"`
    54→	Data       schema.FloatArray `json:"data,omitempty"`
    55→	From       int64             `json:"from"`
    56→	To         int64             `json:"to"`
    57→	Resolution int64             `json:"resolution"`
    58→	Avg        schema.Float      `json:"avg"`
    59→	Min        schema.Float      `json:"min"`
    60→	Max        schema.Float      `json:"max"`
    61→}
    62→
    63→// TODO: Optimize this, just like the stats endpoint!
    64→func (data *APIMetricData) AddStats() {
    65→	n := 0
    66→	sum, min, max := 0.0, math.MaxFloat64, -math.MaxFloat64
    67→	for _, x := range data.Data {
    68→		if x.IsNaN() {
    69→			continue
    70→		}
    71→
    72→		n += 1
    73→		sum += float64(x)
    74→		min = math.Min(min, float64(x))
    75→		max = math.Max(max, float64(x))
    76→	}
    77→
    78→	if n > 0 {
    79→		avg := sum / float64(n)
    80→		data.Avg = schema.Float(avg)
    81→		data.Min = schema.Float(min)
    82→		data.Max = schema.Float(max)
    83→	} else {
    84→		data.Avg, data.Min, data.Max = schema.NaN, schema.NaN, schema.NaN
    85→	}
    86→}
    87→
    88→func (data *APIMetricData) ScaleBy(f schema.Float) {
    89→	if f == 0 || f == 1 {
    90→		return
    91→	}
    92→
    93→	data.Avg *= f
    94→	data.Min *= f
    95→	data.Max *= f
    96→	for i := 0; i < len(data.Data); i++ {
    97→		data.Data[i] *= f
    98→	}
    99→}
   100→
   101→func (data *APIMetricData) PadDataWithNull(ms *metricstore.MemoryStore, from, to int64, metric string) {
   102→	minfo, ok := ms.Metrics[metric]
   103→	if !ok {
   104→		return
   105→	}
   106→
   107→	if (data.From / minfo.Frequency) > (from / minfo.Frequency) {
   108→		padfront := int((data.From / minfo.Frequency) - (from / minfo.Frequency))
   109→		ndata := make([]schema.Float, 0, padfront+len(data.Data))
   110→		for range padfront {
   111→			ndata = append(ndata, schema.NaN)
   112→		}
   113→		for j := 0; j < len(data.Data); j++ {
   114→			ndata = append(ndata, data.Data[j])
   115→		}
   116→		data.Data = ndata
   117→	}
   118→}
   119→
   120→type APIQueryRequest struct {
   121→	Cluster     string     `json:"cluster"`
   122→	Queries     []APIQuery `json:"queries"`
   123→	ForAllNodes []string   `json:"for-all-nodes"`
   124→	From        int64      `json:"from"`
   125→	To          int64      `json:"to"`
   126→	WithStats   bool       `json:"with-stats"`
   127→	WithData    bool       `json:"with-data"`
   128→	WithPadding bool       `json:"with-padding"`
   129→}
   130→
   131→type APIQueryResponse struct {
   132→	Queries []APIQuery        `json:"queries,omitempty"`
   133→	Results [][]APIMetricData `json:"results"`
   134→}
   135→
   136→type APIQuery struct {
   137→	Type        *string      `json:"type,omitempty"`
   138→	SubType     *string      `json:"subtype,omitempty"`
   139→	Metric      string       `json:"metric"`
   140→	Hostname    string       `json:"host"`
   141→	Resolution  int64        `json:"resolution"`
   142→	TypeIds     []string     `json:"type-ids,omitempty"`
   143→	SubTypeIds  []string     `json:"subtype-ids,omitempty"`
   144→	ScaleFactor schema.Float `json:"scale-by,omitempty"`
   145→	Aggregate   bool         `json:"aggreg"`
   146→}
   147→
   148→// handleQuery godoc
   149→// @summary    Query metrics
   150→// @tags query
   151→// @description This endpoint allows the users to retrieve data from the
   152→// in-memory database. The CCMS will return data in JSON format for the
   153→// specified interval requested by the user
   154→// @accept      json
   155→// @produce     json
   156→// @param       request body     api.ApiQueryRequest  true "API query payload object"
   157→// @success     200            {object} api.ApiQueryResponse  "API query response object"
   158→// @failure     400            {object} api.ErrorResponse       "Bad Request"
   159→// @failure     401   		   {object} api.ErrorResponse       "Unauthorized"
   160→// @failure     403            {object} api.ErrorResponse       "Forbidden"
   161→// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
   162→// @security    ApiKeyAuth
   163→// @router      /query/ [get]
   164→func handleQuery(rw http.ResponseWriter, r *http.Request) {
   165→	var err error
   166→	ver := r.URL.Query().Get("version")
   167→	if ver == "" {
   168→		ver = "v2"
   169→	}
   170→	req := APIQueryRequest{WithStats: true, WithData: true, WithPadding: true}
   171→	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
   172→		handleError(err, http.StatusBadRequest, rw)
   173→		return
   174→	}
   175→
   176→	ms := metricstore.GetMemoryStore()
   177→
   178→	response := APIQueryResponse{
   179→		Results: make([][]APIMetricData, 0, len(req.Queries)),
   180→	}
   181→	if req.ForAllNodes != nil {
   182→		nodes := ms.ListChildren([]string{req.Cluster})
   183→		for _, node := range nodes {
   184→			for _, metric := range req.ForAllNodes {
   185→				q := APIQuery{
   186→					Metric:   metric,
   187→					Hostname: node,
   188→				}
   189→				req.Queries = append(req.Queries, q)
   190→				response.Queries = append(response.Queries, q)
   191→			}
   192→		}
   193→	}
   194→
   195→	for _, query := range req.Queries {
   196→		sels := make([]util.Selector, 0, 1)
   197→		if query.Aggregate || query.Type == nil {
   198→			sel := util.Selector{{String: req.Cluster}, {String: query.Hostname}}
   199→			if query.Type != nil {
   200→				if len(query.TypeIds) == 1 {
   201→					sel = append(sel, util.SelectorElement{String: *query.Type + query.TypeIds[0]})
   202→				} else {
   203→					ids := make([]string, len(query.TypeIds))
   204→					for i, id := range query.TypeIds {
   205→						ids[i] = *query.Type + id
   206→					}
   207→					sel = append(sel, util.SelectorElement{Group: ids})
   208→				}
   209→
   210→				if query.SubType != nil {
   211→					if len(query.SubTypeIds) == 1 {
   212→						sel = append(sel, util.SelectorElement{String: *query.SubType + query.SubTypeIds[0]})
   213→					} else {
   214→						ids := make([]string, len(query.SubTypeIds))
   215→						for i, id := range query.SubTypeIds {
   216→							ids[i] = *query.SubType + id
   217→						}
   218→						sel = append(sel, util.SelectorElement{Group: ids})
   219→					}
   220→				}
   221→			}
   222→			sels = append(sels, sel)
   223→		} else {
   224→			for _, typeID := range query.TypeIds {
   225→				if query.SubType != nil {
   226→					for _, subTypeID := range query.SubTypeIds {
   227→						sels = append(sels, util.Selector{
   228→							{String: req.Cluster},
   229→							{String: query.Hostname},
   230→							{String: *query.Type + typeID},
   231→							{String: *query.SubType + subTypeID},
   232→						})
   233→					}
   234→				} else {
   235→					sels = append(sels, util.Selector{
   236→						{String: req.Cluster},
   237→						{String: query.Hostname},
   238→						{String: *query.Type + typeID},
   239→					})
   240→				}
   241→			}
   242→		}
   243→
   244→		// log.Printf("query: %#v\n", query)
   245→		// log.Printf("sels: %#v\n", sels)
   246→
   247→		res := make([]APIMetricData, 0, len(sels))
   248→		for _, sel := range sels {
   249→			data := APIMetricData{}
   250→
   251→			data.Data, data.From, data.To, data.Resolution, err = ms.Read(sel, query.Metric, req.From, req.To, query.Resolution)
   252→			if err != nil {
   253→				// Skip Error If Just Missing Host or Metric, Continue
   254→				// Empty Return For Metric Handled Gracefully By Frontend
   255→				if err != metricstore.ErrNoHostOrMetric {
   256→					msg := err.Error()
   257→					data.Error = &msg
   258→					res = append(res, data)
   259→				} else {
   260→					cclog.Warnf("failed to fetch '%s' from host '%s' (cluster: %s): %s", query.Metric, query.Hostname, req.Cluster, err.Error())
   261→				}
   262→				continue
   263→			}
   264→
   265→			if req.WithStats {
   266→				data.AddStats()
   267→			}
   268→			if query.ScaleFactor != 0 {
   269→				data.ScaleBy(query.ScaleFactor)
   270→			}
   271→			if req.WithPadding {
   272→				data.PadDataWithNull(ms, req.From, req.To, query.Metric)
   273→			}
   274→			if !req.WithData {
   275→				data.Data = nil
   276→			}
   277→			res = append(res, data)
   278→		}
   279→		response.Results = append(response.Results, res)
   280→	}
   281→
   282→	rw.Header().Set("Content-Type", "application/json")
   283→	bw := bufio.NewWriter(rw)
   284→	defer bw.Flush()
   285→	if err := json.NewEncoder(bw).Encode(response); err != nil {
   286→		log.Print(err)
   287→		return
   288→	}
   289→}
   290→
   291→// handleFree godoc
   292→// @summary
   293→// @tags free
   294→// @description This endpoint allows the users to free the Buffers from the
   295→// metric store. This endpoint offers the users to remove then systematically
   296→// and also allows then to prune the data under node, if they do not want to
   297→// remove the whole node.
   298→// @produce     json
   299→// @param       to        query    string        false  "up to timestamp"
   300→// @success     200            {string} string  "ok"
   301→// @failure     400            {object} api.ErrorResponse       "Bad Request"
   302→// @failure     401            {object} api.ErrorResponse       "Unauthorized"
   303→// @failure     403            {object} api.ErrorResponse       "Forbidden"
   304→// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
   305→// @security    ApiKeyAuth
   306→// @router      /free/ [post]
   307→func freeMetrics(rw http.ResponseWriter, r *http.Request) {
   308→	rawTo := r.URL.Query().Get("to")
   309→	if rawTo == "" {
   310→		handleError(errors.New("'to' is a required query parameter"), http.StatusBadRequest, rw)
   311→		return
   312→	}
   313→
   314→	to, err := strconv.ParseInt(rawTo, 10, 64)
   315→	if err != nil {
   316→		handleError(err, http.StatusInternalServerError, rw)
   317→		return
   318→	}
   319→
   320→	bodyDec := json.NewDecoder(r.Body)
   321→	var selectors [][]string
   322→	err = bodyDec.Decode(&selectors)
   323→	if err != nil {
   324→		http.Error(rw, err.Error(), http.StatusBadRequest)
   325→		return
   326→	}
   327→
   328→	ms := metricstore.GetMemoryStore()
   329→	n := 0
   330→	for _, sel := range selectors {
   331→		bn, err := ms.Free(sel, to)
   332→		if err != nil {
   333→			handleError(err, http.StatusInternalServerError, rw)
   334→			return
   335→		}
   336→
   337→		n += bn
   338→	}
   339→
   340→	rw.WriteHeader(http.StatusOK)
   341→	fmt.Fprintf(rw, "buffers freed: %d\n", n)
   342→}
   343→
   344→// handleWrite godoc
   345→// @summary Receive metrics in InfluxDB line-protocol
   346→// @tags write
   347→// @description Write data to the in-memory store in the InfluxDB line-protocol using [this format](https://github.com/ClusterCockpit/cc-specifications/blob/master/metrics/lineprotocol_alternative.md)
   348→
   349→// @accept      plain
   350→// @produce     json
   351→// @param       cluster        query string false "If the lines in the body do not have a cluster tag, use this value instead."
   352→// @success     200            {string} string  "ok"
   353→// @failure     400            {object} api.ErrorResponse       "Bad Request"
   354→// @failure     401            {object} api.ErrorResponse       "Unauthorized"
   355→// @failure     403            {object} api.ErrorResponse       "Forbidden"
   356→// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
   357→// @security    ApiKeyAuth
   358→// @router      /write/ [post]
   359→func writeMetrics(rw http.ResponseWriter, r *http.Request) {
   360→	rw.Header().Add("Content-Type", "application/json")
   361→
   362→	// Extract the "cluster" query parameter without allocating a url.Values map.
   363→	cluster := queryParam(r.URL.RawQuery, "cluster")
   364→
   365→	// Stream directly from the request body instead of copying it into a
   366→	// temporary buffer via io.ReadAll. The line-protocol decoder supports
   367→	// io.Reader natively, so this avoids the largest heap allocation.
   368→	ms := metricstore.GetMemoryStore()
   369→	dec := lineprotocol.NewDecoder(r.Body)
   370→	if err := metricstore.DecodeLine(dec, ms, cluster); err != nil {
   371→		cclog.Errorf("/api/write error: %s", err.Error())
   372→		handleError(err, http.StatusBadRequest, rw)
   373→		return
   374→	}
   375→	rw.WriteHeader(http.StatusOK)
   376→}
   377→
   378→// handleDebug godoc
   379→// @summary Debug endpoint
   380→// @tags debug
   381→// @description This endpoint allows the users to print the content of
   382→// nodes/clusters/metrics to review the state of the data.
   383→// @produce     json
   384→// @param       selector        query    string            false "Selector"
   385→// @success     200            {string} string  "Debug dump"
   386→// @failure     400            {object} api.ErrorResponse       "Bad Request"
   387→// @failure     401            {object} api.ErrorResponse       "Unauthorized"
   388→// @failure     403            {object} api.ErrorResponse       "Forbidden"
   389→// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
   390→// @security    ApiKeyAuth
   391→// @router      /debug/ [post]
   392→func debugMetrics(rw http.ResponseWriter, r *http.Request) {
   393→	raw := r.URL.Query().Get("selector")
   394→	rw.Header().Add("Content-Type", "application/json")
   395→	selector := []string{}
   396→	if len(raw) != 0 {
   397→		selector = strings.Split(raw, ":")
   398→	}
   399→
   400→	ms := metricstore.GetMemoryStore()
   401→	if err := ms.DebugDump(bufio.NewWriter(rw), selector); err != nil {
   402→		handleError(err, http.StatusBadRequest, rw)
   403→		return
   404→	}
   405→}
   406→
   407→// handleHealthCheck godoc
   408→// @summary HealthCheck endpoint
   409→// @tags healthcheck
   410→// @description This endpoint allows the users to check if a node is healthy
   411→// @produce     json
   412→// @param       selector        query    string            false "Selector"
   413→// @success     200            {string} string  "Debug dump"
   414→// @failure     400            {object} api.ErrorResponse       "Bad Request"
   415→// @failure     401            {object} api.ErrorResponse       "Unauthorized"
   416→// @failure     403            {object} api.ErrorResponse       "Forbidden"
   417→// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
   418→// @security    ApiKeyAuth
   419→// @router      /healthcheck/ [get]
   420→func metricsHealth(rw http.ResponseWriter, r *http.Request) {
   421→	req := metricstore.HealthCheckReq{}
   422→
   423→	dec := json.NewDecoder(r.Body)
   424→	dec.DisallowUnknownFields()
   425→
   426→	if err := dec.Decode(&req); err != nil {
   427→		handleError(fmt.Errorf("parsing request body failed: %w", err),
   428→			http.StatusBadRequest, rw)
   429→		return
   430→	}
   431→
   432→	rw.Header().Add("Content-Type", "application/json")
   433→
   434→	ms := metricstore.GetMemoryStore()
   435→	results, err := ms.HealthCheck(req.Cluster, req.Nodes, req.MetricNames)
   436→	if err != nil {
   437→		handleError(err, http.StatusBadRequest, rw)
   438→		return
   439→	}
   440→
   441→	rw.Header().Set("Content-Type", "application/json")
   442→	bw := bufio.NewWriter(rw)
   443→	defer bw.Flush()
   444→	if err := json.NewEncoder(bw).Encode(results); err != nil {
   445→		log.Print(err)
   446→		return
   447→	}
   448→}
   449→
   450→// queryParam extracts a single query parameter value from a raw query string
   451→// without allocating a url.Values map.
   452→func queryParam(rawQuery, key string) string {
   453→	for rawQuery != "" {
   454→		var kv string
   455→		if before, after, ok := strings.Cut(rawQuery, "&"); ok {
   456→			kv, rawQuery = before, after
   457→		} else {
   458→			kv, rawQuery = rawQuery, ""
   459→		}
   460→		if before, after, ok := strings.Cut(kv, "="); ok {
   461→			if before == key {
   462→				return after
   463→			}
   464→		} else if kv == key {
   465→			return ""
   466→		}
   467→	}
   468→	return ""
   469→}
   470→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-metric-store.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package api

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"math"
	"net/http"
	"strconv"
	"strings"

	"github.com/ClusterCockpit/cc-backend/pkg/metricstore"
	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
	"github.com/ClusterCockpit/cc-lib/v2/schema"
	"github.com/ClusterCockpit/cc-lib/v2/util"
	"github.com/ClusterCockpit/cc-line-protocol/v2/lineprotocol"
)

// ErrorResponse model
type ErrorResponse struct {
	// Statustext of Errorcode
	Status string `json:"status"`
	Error  string `json:"error"` // Error Message
}

// DefaultAPIResponse model
type DefaultAPIResponse struct {
	Message string `json:"msg"`
}

// handleError writes a standardized JSON error response with the given status code.
// It logs the error at WARN level and ensures proper Content-Type headers are set.
func handleError(err error, statusCode int, rw http.ResponseWriter) {
	cclog.Warnf("REST ERROR : %s", err.Error())
	rw.Header().Add("Content-Type", "application/json")
	rw.WriteHeader(statusCode)
	if err := json.NewEncoder(rw).Encode(ErrorResponse{
		Status: http.StatusText(statusCode),
		Error:  err.Error(),
	}); err != nil {
		cclog.Errorf("Failed to encode error response: %v", err)
	}
}

type APIMetricData struct {
	Error      *string           `json:"error,omitempty"`
	Data       schema.FloatArray `json:"data,omitempty"`
	From       int64             `json:"from"`
	To         int64             `json:"to"`
	Resolution int64             `json:"resolution"`
	Avg        schema.Float      `json:"avg"`
	Min        schema.Float      `json:"min"`
	Max        schema.Float      `json:"max"`
}

// TODO: Optimize this, just like the stats endpoint!
func (data *APIMetricData) AddStats() {
	n := 0
	sum, min, max := 0.0, math.MaxFloat64, -math.MaxFloat64
	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))
	}

	if n > 0 {
		avg := sum / float64(n)
		data.Avg = schema.Float(avg)
		data.Min = schema.Float(min)
		data.Max = schema.Float(max)
	} else {
		data.Avg, data.Min, data.Max = schema.NaN, schema.NaN, schema.NaN
	}
}

func (data *APIMetricData) ScaleBy(f schema.Float) {
	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
	}
}

func (data *APIMetricData) PadDataWithNull(ms *metricstore.MemoryStore, from, to int64, metric string) {
	minfo, ok := ms.Metrics[metric]
	if !ok {
		return
	}

	if (data.From / minfo.Frequency) > (from / minfo.Frequency) {
		padfront := int((data.From / minfo.Frequency) - (from / minfo.Frequency))
		ndata := make([]schema.Float, 0, padfront+len(data.Data))
		for range padfront {
			ndata = append(ndata, schema.NaN)
		}
		for j := 0; j < len(data.Data); j++ {
			ndata = append(ndata, data.Data[j])
		}
		data.Data = ndata
	}
}

type APIQueryRequest struct {
	Cluster     string     `json:"cluster"`
	Queries     []APIQuery `json:"queries"`
	ForAllNodes []string   `json:"for-all-nodes"`
	From        int64      `json:"from"`
	To          int64      `json:"to"`
	WithStats   bool       `json:"with-stats"`
	WithData    bool       `json:"with-data"`
	WithPadding bool       `json:"with-padding"`
}

type APIQueryResponse struct {
	Queries []APIQuery        `json:"queries,omitempty"`
	Results [][]APIMetricData `json:"results"`
}

type APIQuery struct {
	Type        *string      `json:"type,omitempty"`
	SubType     *string      `json:"subtype,omitempty"`
	Metric      string       `json:"metric"`
	Hostname    string       `json:"host"`
	Resolution  int64        `json:"resolution"`
	TypeIds     []string     `json:"type-ids,omitempty"`
	SubTypeIds  []string     `json:"subtype-ids,omitempty"`
	ScaleFactor schema.Float `json:"scale-by,omitempty"`
	Aggregate   bool         `json:"aggreg"`
}

// handleQuery godoc
// @summary    Query metrics
// @tags query
// @description This endpoint allows the users to retrieve data from the
// in-memory database. The CCMS will return data in JSON format for the
// specified interval requested by the user
// @accept      json
// @produce     json
// @param       request body     api.ApiQueryRequest  true "API query payload object"
// @success     200            {object} api.ApiQueryResponse  "API query response object"
// @failure     400            {object} api.ErrorResponse       "Bad Request"
// @failure     401   		   {object} api.ErrorResponse       "Unauthorized"
// @failure     403            {object} api.ErrorResponse       "Forbidden"
// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
// @security    ApiKeyAuth
// @router      /query/ [get]
func handleQuery(rw http.ResponseWriter, r *http.Request) {
	var err error
	ver := r.URL.Query().Get("version")
	if ver == "" {
		ver = "v2"
	}
	req := APIQueryRequest{WithStats: true, WithData: true, WithPadding: true}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		handleError(err, http.StatusBadRequest, rw)
		return
	}

	ms := metricstore.GetMemoryStore()

	response := APIQueryResponse{
		Results: make([][]APIMetricData, 0, len(req.Queries)),
	}
	if req.ForAllNodes != nil {
		nodes := ms.ListChildren([]string{req.Cluster})
		for _, node := range nodes {
			for _, metric := range req.ForAllNodes {
				q := APIQuery{
					Metric:   metric,
					Hostname: node,
				}
				req.Queries = append(req.Queries, q)
				response.Queries = append(response.Queries, q)
			}
		}
	}

	for _, query := range req.Queries {
		sels := make([]util.Selector, 0, 1)
		if query.Aggregate || query.Type == nil {
			sel := util.Selector{{String: req.Cluster}, {String: query.Hostname}}
			if query.Type != nil {
				if len(query.TypeIds) == 1 {
					sel = append(sel, util.SelectorElement{String: *query.Type + query.TypeIds[0]})
				} else {
					ids := make([]string, len(query.TypeIds))
					for i, id := range query.TypeIds {
						ids[i] = *query.Type + id
					}
					sel = append(sel, util.SelectorElement{Group: ids})
				}

				if query.SubType != nil {
					if len(query.SubTypeIds) == 1 {
						sel = append(sel, util.SelectorElement{String: *query.SubType + query.SubTypeIds[0]})
					} else {
						ids := make([]string, len(query.SubTypeIds))
						for i, id := range query.SubTypeIds {
							ids[i] = *query.SubType + id
						}
						sel = append(sel, util.SelectorElement{Group: ids})
					}
				}
			}
			sels = append(sels, sel)
		} else {
			for _, typeID := range query.TypeIds {
				if query.SubType != nil {
					for _, subTypeID := range query.SubTypeIds {
						sels = append(sels, util.Selector{
							{String: req.Cluster},
							{String: query.Hostname},
							{String: *query.Type + typeID},
							{String: *query.SubType + subTypeID},
						})
					}
				} else {
					sels = append(sels, util.Selector{
						{String: req.Cluster},
						{String: query.Hostname},
						{String: *query.Type + typeID},
					})
				}
			}
		}

		// log.Printf("query: %#v\n", query)
		// log.Printf("sels: %#v\n", sels)

		res := make([]APIMetricData, 0, len(sels))
		for _, sel := range sels {
			data := APIMetricData{}

			data.Data, data.From, data.To, data.Resolution, err = ms.Read(sel, query.Metric, req.From, req.To, query.Resolution)
			if err != nil {
				// Skip Error If Just Missing Host or Metric, Continue
				// Empty Return For Metric Handled Gracefully By Frontend
				if err != metricstore.ErrNoHostOrMetric {
					msg := err.Error()
					data.Error = &msg
					res = append(res, data)
				} else {
					cclog.Warnf("failed to fetch '%s' from host '%s' (cluster: %s): %s", query.Metric, query.Hostname, req.Cluster, err.Error())
				}
				continue
			}

			if req.WithStats {
				data.AddStats()
			}
			if query.ScaleFactor != 0 {
				data.ScaleBy(query.ScaleFactor)
			}
			if req.WithPadding {
				data.PadDataWithNull(ms, req.From, req.To, query.Metric)
			}
			if !req.WithData {
				data.Data = nil
			}
			res = append(res, data)
		}
		response.Results = append(response.Results, res)
	}

	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
	}
}

// handleFree godoc
// @summary
// @tags free
// @description This endpoint allows the users to free the Buffers from the
// metric store. This endpoint offers the users to remove then systematically
// and also allows then to prune the data under node, if they do not want to
// remove the whole node.
// @produce     json
// @param       to        query    string        false  "up to timestamp"
// @success     200            {string} string  "ok"
// @failure     400            {object} api.ErrorResponse       "Bad Request"
// @failure     401            {object} api.ErrorResponse       "Unauthorized"
// @failure     403            {object} api.ErrorResponse       "Forbidden"
// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
// @security    ApiKeyAuth
// @router      /free/ [post]
func freeMetrics(rw http.ResponseWriter, r *http.Request) {
	rawTo := r.URL.Query().Get("to")
	if rawTo == "" {
		handleError(errors.New("'to' is a required query parameter"), http.StatusBadRequest, rw)
		return
	}

	to, err := strconv.ParseInt(rawTo, 10, 64)
	if err != nil {
		handleError(err, http.StatusInternalServerError, rw)
		return
	}

	bodyDec := json.NewDecoder(r.Body)
	var selectors [][]string
	err = bodyDec.Decode(&selectors)
	if err != nil {
		http.Error(rw, err.Error(), http.StatusBadRequest)
		return
	}

	ms := metricstore.GetMemoryStore()
	n := 0
	for _, sel := range selectors {
		bn, err := ms.Free(sel, to)
		if err != nil {
			handleError(err, http.StatusInternalServerError, rw)
			return
		}

		n += bn
	}

	rw.WriteHeader(http.StatusOK)
	fmt.Fprintf(rw, "buffers freed: %d\n", n)
}

// handleWrite godoc
// @summary Receive metrics in InfluxDB line-protocol
// @tags write
// @description Write data to the in-memory store in the InfluxDB line-protocol using [this format](https://github.com/ClusterCockpit/cc-specifications/blob/master/metrics/lineprotocol_alternative.md)

// @accept      plain
// @produce     json
// @param       cluster        query string false "If the lines in the body do not have a cluster tag, use this value instead."
// @success     200            {string} string  "ok"
// @failure     400            {object} api.ErrorResponse       "Bad Request"
// @failure     401            {object} api.ErrorResponse       "Unauthorized"
// @failure     403            {object} api.ErrorResponse       "Forbidden"
// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
// @security    ApiKeyAuth
// @router      /write/ [post]
func writeMetrics(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Add("Content-Type", "application/json")

	// Extract the "cluster" query parameter without allocating a url.Values map.
	cluster := queryParam(r.URL.RawQuery, "cluster")

	// Stream directly from the request body instead of copying it into a
	// temporary buffer via io.ReadAll. The line-protocol decoder supports
	// io.Reader natively, so this avoids the largest heap allocation.
	ms := metricstore.GetMemoryStore()
	dec := lineprotocol.NewDecoder(r.Body)
	if err := metricstore.DecodeLine(dec, ms, cluster); err != nil {
		cclog.Errorf("/api/write error: %s", err.Error())
		handleError(err, http.StatusBadRequest, rw)
		return
	}
	rw.WriteHeader(http.StatusOK)
}

// handleDebug godoc
// @summary Debug endpoint
// @tags debug
// @description This endpoint allows the users to print the content of
// nodes/clusters/metrics to review the state of the data.
// @produce     json
// @param       selector        query    string            false "Selector"
// @success     200            {string} string  "Debug dump"
// @failure     400            {object} api.ErrorResponse       "Bad Request"
// @failure     401            {object} api.ErrorResponse       "Unauthorized"
// @failure     403            {object} api.ErrorResponse       "Forbidden"
// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
// @security    ApiKeyAuth
// @router      /debug/ [post]
func debugMetrics(rw http.ResponseWriter, r *http.Request) {
	raw := r.URL.Query().Get("selector")
	rw.Header().Add("Content-Type", "application/json")
	selector := []string{}
	if len(raw) != 0 {
		selector = strings.Split(raw, ":")
	}

	ms := metricstore.GetMemoryStore()
	if err := ms.DebugDump(bufio.NewWriter(rw), selector); err != nil {
		handleError(err, http.StatusBadRequest, rw)
		return
	}
}

// handleHealthCheck godoc
// @summary HealthCheck endpoint
// @tags healthcheck
// @description This endpoint allows the users to check if a node is healthy
// @produce     json
// @param       selector        query    string            false "Selector"
// @success     200            {string} string  "Debug dump"
// @failure     400            {object} api.ErrorResponse       "Bad Request"
// @failure     401            {object} api.ErrorResponse       "Unauthorized"
// @failure     403            {object} api.ErrorResponse       "Forbidden"
// @failure     500            {object} api.ErrorResponse       "Internal Server Error"
// @security    ApiKeyAuth
// @router      /healthcheck/ [get]
func metricsHealth(rw http.ResponseWriter, r *http.Request) {
	req := metricstore.HealthCheckReq{}

	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()

	if err := dec.Decode(&req); err != nil {
		handleError(fmt.Errorf("parsing request body failed: %w", err),
			http.StatusBadRequest, rw)
		return
	}

	rw.Header().Add("Content-Type", "application/json")

	ms := metricstore.GetMemoryStore()
	results, err := ms.HealthCheck(req.Cluster, req.Nodes, req.MetricNames)
	if err != nil {
		handleError(err, http.StatusBadRequest, rw)
		return
	}

	rw.Header().Set("Content-Type", "application/json")
	bw := bufio.NewWriter(rw)
	defer bw.Flush()
	if err := json.NewEncoder(bw).Encode(results); err != nil {
		log.Print(err)
		return
	}
}

// queryParam extracts a single query parameter value from a raw query string
// without allocating a url.Values map.
func queryParam(rawQuery, key string) string {
	for rawQuery != "" {
		var kv string
		if before, after, ok := strings.Cut(rawQuery, "&"); ok {
			kv, rawQuery = before, after
		} else {
			kv, rawQuery = rawQuery, ""
		}
		if before, after, ok := strings.Cut(kv, "="); ok {
			if before == key {
				return after
			}
		} else if kv == key {
			return ""
		}
	}
	return ""
}


Now let me check the current swaggo/swag version and http-swagger versions, and see what the latest releases are.


Now let me check the latest versions available:


Bash

go list -m -u github.com/swaggo/swag github.com/swaggo/http-swagger github.com/swaggo/files 2>&1

Check current swaggo versions and available updates


github.com/swaggo/swag v1.16.6
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/files v1.0.1


go list -m -versions github.com/swaggo/swag 2>&1 | tr ' ' '\n' | tail -10

List available swag versions


v1.8.9
v1.8.10
v1.8.11
v1.8.12
v1.16.1
v1.16.2
v1.16.3
v1.16.4
v1.16.5
v1.16.6


go list -m -versions github.com/swaggo/http-swagger 2>&1 | tr ' ' '\n' | tail -10

List available http-swagger versions


v1.2.1
v1.2.5
v1.2.6
v1.2.7
v1.2.8
v1.3.0
v1.3.1
v1.3.2
v1.3.3
v1.3.4


Both swaggo/swag and swaggo/http-swagger are already at their latest versions (v1.16.6 and v1.3.4 respectively). But the user mentioned "integrate swaggo/swag and update to latest release". Let me check if there's a v2 or newer major version of swaggo/swag.

Actually, looking at the Makefile, the swagger target exists but uses `go run github.com/swaggo/swag/cmd/swag init`. The swaggo integration looks already in place.

Wait, let me check if there's a v2 version for swaggo packages.

The user might be referring to a newer major version that I should look into.


go list -m -versions github.com/swaggo/files 2>&1 | tr ' ' '\n' | tail -10

List available swaggo/files versions


github.com/swaggo/files
v1.0.0
v1.0.1


GOPATH=$(go env GOPATH) && curl -s https://proxy.golang.org/github.com/swaggo/swag/@v/list 2>&1 | tr ',' '\n' | tail -20

Check latest swag versions from Go proxy


v1.7.3
v1.8.9-rc3
v1.7.5
v1.3.2
v1.4.0
v1.6.1
v1.7.7
v1.7.9-p1
v1.16.6
v1.6.6
v1.8.1
v1.6.2
v1.7.4
v1.16.0
v1.0.1
v1.1.0
v1.8.4
v1.6.7
v1.7.6
v1.16.5


curl -s https://proxy.golang.org/github.com/swaggo/http-swagger/v2/@v/list 2>&1 | head -20

Check if http-swagger v2 exists


curl -s https://proxy.golang.org/github.com/swaggo/swag/v2/@v/list 2>&1 | head -20

Check if swag v2 exists


v2.0.2
v2.0.0
v2.0.1


v2.0.0-beta
v2.0.0-rc5
v2.0.0-rc4
v2.0.0-rc3
v2.0.0-rc1
v2.0.0-rc2


There's a v2 of `http-swagger` (`v2.0.2`) and a v2 beta of `swag`. The user wants to update to the latest release. The latest stable for swaggo/swag is v1.16.6 (already at this version). For http-swagger, there's v2.0.2.

Let me check what files/swaggo v2 has:


curl -s https://proxy.golang.org/github.com/swaggo/files/v2/@v/list 2>&1 | head -20

Check if swaggo/files v2 exists


So there are v2 versions of:
- `github.com/swaggo/http-swagger/v2` → v2.0.2
- `github.com/swaggo/files/v2` → v2.0.2

But `swaggo/swag` only has a beta v2 (`v2.0.0-rc5`), so the latest stable is v1.16.6.

Let me check what's in http-swagger v2 to understand the API differences. The import path changes, so I need to update that.

Actually, the user says "integrate swaggo/swag and update to latest release". Looking at what's already in the code:

The current setup is using older versions across the board—swag v1.16.6, http-swagger v1.3.4, and files v1.0.1—but v2 releases are available for the latter two. The Makefile already has a swagger generation target that runs the swag init command and moves the generated docs file, though it looks like the command might be incomplete.

I need to verify whether the `api.go` file that the `-g` flag is looking for actually exists in the specified directories, since that's the main file where Swagger annotations should be defined.


List api directory contents


authentication.go
docs.go
metricstore.go
nodeprovider.go
server.go


ls /Users/jan/prg/cc-metric-store/internal/util/ 2>/dev/null || echo "no util dir"

Check if util directory exists


no util dir


There's no `internal/util` directory and no `api.go` file. The Makefile refers to `-g api.go` - but the Makefile has been modified (M Makefile). Let me look at what files are changed.

Looking at the situation:
1. `swaggo/swag` is at v1.16.6 (latest stable, v2 only has beta)
2. `swaggo/http-swagger` can be upgraded from v1.3.4 to v2.0.2
3. `swaggo/files` can be upgraded from v1.0.1 to v2.0.2

4. The http-swagger v2 upgrade changes the import path to include `/v2`, and I need to update the Makefile's swagger command since it's pointing to directories and files that no longer exist in the current structure.

Looking at the Makefile, the `swag init` command references `./internal/util` and `api.go` which aren't present anymore. The swagger annotations in `metricstore.go` exist, but there's no file with the general info annotations like `@title` and `@version` that `swag init` expects to find. The `docs.go` file has hardcoded SwaggerInfo values, so I need to figure out where those annotations should live or if I need to create a file to hold them.

For the user's goal of integrating swaggo/swag and updating to the latest release, I should update the http-swagger and swaggo/files dependencies to v2, fix the Makefile swagger target to point to the right location, and ensure the general swagger annotations are properly set up.


Let me check what http-swagger v2 API looks like:


cd /tmp && go doc github.com/swaggo/http-swagger/v2@v2.0.2 2>&1 | head -30 || true

Check http-swagger v2 package docs


go get github.com/swaggo/http-swagger/v2@v2.0.2 github.com/swaggo/files/v2@v2.0.2 2>&1

Update swaggo packages to v2


doc: cannot find package "github.com/swaggo/http-swagger/v2@v2.0.2" in any of:
	($GOROOT not set)
	/Users/jan/go/src/github.com/swaggo/http-swagger/v2@v2.0.2 (from $GOPATH)
Shell cwd was reset to /Users/jan/prg/cc-metric-store


go: downloading github.com/swaggo/http-swagger/v2 v2.0.2
go: downloading github.com/swaggo/files/v2 v2.0.2
go: added github.com/swaggo/files/v2 v2.0.2
go: added github.com/swaggo/http-swagger/v2 v2.0.2


cat $(go env GOPATH)/pkg/mod/github.com/swaggo/http-swagger/v2@v2.0.2/*.go 2>/dev/null | head -60

Check http-swagger v2 source to understand API changes