package httpSwagger

import (
	"bytes"
	"html/template"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/swaggo/swag"
)

type mockedSwag struct{}

func (s *mockedSwag) ReadDoc() string {
	return `{
    "swagger": "2.0",
    "info": {
        "description": "This is a sample server Petstore server.",
        "title": "Swagger Example API",
        "termsOfService": "http://swagger.io/terms/",
        "contact": {
            "name": "API Support",
            "url": "http://www.swagger.io/support",
            "email": "support@swagger.io"
        },
        "license": {
            "name": "Apache 2.0",
            "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
        },
        "version": "1.0"
    },
    "host": "petstore.swagger.io",
    "basePath": "/v2",
    "paths": {}
}`
}

func TestWrapHandler(t *testing.T) {

	tests := []struct {
		RootFolder   string
		InstanceName string
	}{
		{
			RootFolder:   "/",
			InstanceName: "default",
		},

		{
			RootFolder:   "/swagger/",
			InstanceName: "swagger",
		},
		{
			RootFolder:   "/custom/",
			InstanceName: "custom",
		},


Bash

ls $(go env GOPATH)/pkg/mod/github.com/swaggo/http-swagger/v2@v2.0.2/

List http-swagger v2 files


go.mod
go.sum
LICENSE
PULL_REQUEST_TEMPLATE.md
README.md
swagger.go
swagger_test.go


cat $(go env GOPATH)/pkg/mod/github.com/swaggo/http-swagger/v2@v2.0.2/swagger.go

Read http-swagger v2 swagger.go


package httpSwagger

import (
	"html/template"
	"net/http"
	"net/url"
	"path/filepath"
	"regexp"

	swaggerFiles "github.com/swaggo/files/v2"
	"github.com/swaggo/swag"
)

// WrapHandler wraps swaggerFiles.Handler and returns http.HandlerFunc.
var WrapHandler = Handler()

// Config stores httpSwagger configuration variables.
type Config struct {
	// The url pointing to API definition (normally swagger.json or swagger.yaml). Default is `doc.json`.
	URL                      string
	DocExpansion             string
	DomID                    string
	InstanceName             string
	BeforeScript             template.JS
	AfterScript              template.JS
	Plugins                  []template.JS
	UIConfig                 map[template.JS]template.JS
	DeepLinking              bool
	PersistAuthorization     bool
	Layout                   SwaggerLayout
	DefaultModelsExpandDepth ModelsExpandDepthType
}

// URL presents the url pointing to API definition (normally swagger.json or swagger.yaml).
func URL(url string) func(*Config) {
	return func(c *Config) {
		c.URL = url
	}
}

// DeepLinking true, false.
func DeepLinking(deepLinking bool) func(*Config) {
	return func(c *Config) {
		c.DeepLinking = deepLinking
	}
}

// DocExpansion list, full, none.
func DocExpansion(docExpansion string) func(*Config) {
	return func(c *Config) {
		c.DocExpansion = docExpansion
	}
}

// DomID #swagger-ui.
func DomID(domID string) func(*Config) {
	return func(c *Config) {
		c.DomID = domID
	}
}

// InstanceName set the instance name that was used to generate the swagger documents
// Defaults to swag.Name ("swagger").
func InstanceName(name string) func(*Config) {
	return func(c *Config) {
		c.InstanceName = name
	}
}

// PersistAuthorization Persist authorization information over browser close/refresh.
// Defaults to false.
func PersistAuthorization(persistAuthorization bool) func(*Config) {
	return func(c *Config) {
		c.PersistAuthorization = persistAuthorization
	}
}

// Plugins specifies additional plugins to load into Swagger UI.
func Plugins(plugins []string) func(*Config) {
	return func(c *Config) {
		vs := make([]template.JS, len(plugins))
		for i, v := range plugins {
			vs[i] = template.JS(v)
		}
		c.Plugins = vs
	}
}

// UIConfig specifies additional SwaggerUIBundle config object properties.
func UIConfig(props map[string]string) func(*Config) {
	return func(c *Config) {
		vs := make(map[template.JS]template.JS, len(props))
		for k, v := range props {
			vs[template.JS(k)] = template.JS(v)
		}
		c.UIConfig = vs
	}
}

// BeforeScript holds JavaScript to be run right before the Swagger UI object is created.
func BeforeScript(js string) func(*Config) {
	return func(c *Config) {
		c.BeforeScript = template.JS(js)
	}
}

// AfterScript holds JavaScript to be run right after the Swagger UI object is created
// and set on the window.
func AfterScript(js string) func(*Config) {
	return func(c *Config) {
		c.AfterScript = template.JS(js)
	}
}

type SwaggerLayout string

const (
	BaseLayout       SwaggerLayout = "BaseLayout"
	StandaloneLayout SwaggerLayout = "StandaloneLayout"
)

// Define Layout options are BaseLayout or StandaloneLayout
func Layout(layout SwaggerLayout) func(*Config) {
	return func(c *Config) {
		c.Layout = layout
	}
}

type ModelsExpandDepthType int

const (
	ShowModel ModelsExpandDepthType = 1
	HideModel ModelsExpandDepthType = -1
)

// DefaultModelsExpandDepth presents the model of response and request.
// set the default expansion depth for models
func DefaultModelsExpandDepth(defaultModelsExpandDepth ModelsExpandDepthType) func(*Config) {
	return func(c *Config) {
		c.DefaultModelsExpandDepth = defaultModelsExpandDepth
	}
}

func newConfig(configFns ...func(*Config)) *Config {
	config := Config{
		URL:                      "doc.json",
		DocExpansion:             "list",
		DomID:                    "swagger-ui",
		InstanceName:             "swagger",
		DeepLinking:              true,
		PersistAuthorization:     false,
		Layout:                   StandaloneLayout,
		DefaultModelsExpandDepth: ShowModel,
	}

	for _, fn := range configFns {
		fn(&config)
	}

	if config.InstanceName == "" {
		config.InstanceName = swag.Name
	}

	return &config
}

// Handler wraps `http.Handler` into `http.HandlerFunc`.
func Handler(configFns ...func(*Config)) http.HandlerFunc {

	config := newConfig(configFns...)

	// create a template with name
	index, _ := template.New("swagger_index.html").Parse(indexTempl)

	re := regexp.MustCompile(`^(.*/)([^?].*)?[?|.]*$`)

	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)

			return
		}

		matches := re.FindStringSubmatch(r.RequestURI)

		path := matches[2]

		switch filepath.Ext(path) {
		case ".html":
			w.Header().Set("Content-Type", "text/html; charset=utf-8")

		case ".css":
			w.Header().Set("Content-Type", "text/css; charset=utf-8")
		case ".js":
			w.Header().Set("Content-Type", "application/javascript")
		case ".png":
			w.Header().Set("Content-Type", "image/png")
		case ".json":
			w.Header().Set("Content-Type", "application/json; charset=utf-8")
		}

		switch path {
		case "index.html":
			_ = index.Execute(w, config)
		case "doc.json":
			doc, err := swag.ReadDoc(config.InstanceName)
			if err != nil {
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

				return
			}

			_, _ = w.Write([]byte(doc))
		case "":
			http.Redirect(w, r, matches[1]+"/"+"index.html", http.StatusMovedPermanently)
		default:
			var err error
			r.URL, err = url.Parse(matches[2])
			if err != nil {
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

				return
			}
			http.FileServer(http.FS(swaggerFiles.FS)).ServeHTTP(w, r)
		}
	}
}

const indexTempl = `
<!DOCTYPE html>

  <meta charset="UTF-8">
  <title>Swagger UI</title>
  <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
  <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
  <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
  <defs>
    <symbol viewBox="0 0 20 20" id="unlocked">
      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
    </symbol>

    <symbol viewBox="0 0 20 20" id="locked">
      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
    </symbol>

    <symbol viewBox="0 0 20 20" id="close">
      <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
    </symbol>

    <symbol viewBox="0 0 20 20" id="large-arrow">
      <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
    </symbol>

    <symbol viewBox="0 0 20 20" id="large-arrow-down">
      <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
    </symbol>

    <symbol viewBox="0 0 24 24" id="jump-to">
      <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
    </symbol>

    <symbol viewBox="0 0 24 24" id="expand">
      <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
    </symbol>
  </defs>
</svg>

`


Now I understand the http-swagger v2 API. The `Handler` function signature is the same, just uses a different import path. The main differences:
1. Import path changes from `github.com/swaggo/http-swagger` to `github.com/swaggo/http-swagger/v2`
2. The `httpSwagger.URL()` function is still there

Now I also need to check if the old v1 packages need to be removed and the Makefile needs to be fixed. Let me also look at what needs to change:

1. Update import in `cmd/cc-metric-store/server.go` from `github.com/swaggo/http-swagger` to `github.com/swaggo/http-swagger/v2`

2. Remove old v1 packages from go.mod
3. Fix the Makefile `swagger` target - the `-d ./internal/util` doesn't exist

Looking at the Makefile more carefully, the `swagger` target outputs to `./api` then moves `docs.go` to `./internal/api/docs.go`, but there's no `api.go` file containing the general info annotations that swag needs. The docs.go appears to be manually maintained, so I need to figure out where the `@title` and other metadata should come from for the swag command to work properly.

The `./api` directory likely contains the static `swagger.json` and `swagger.yaml` artifacts. Since the docs.go has the swagger info hardcoded (version, host, basePath, title, description) but there's no corresponding `api.go` file with general info annotations, the swag command would either fail or use defaults. Let me check if there's an `api` directory at the root that I'm missing.


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

Check api directory at root


swagger.json
swagger.yaml


go get -u github.com/swaggo/swag@v1.16.6 2>&1 | head -5

Ensure swag is up to date


Now I understand the full picture:

1. The `api/` directory at the root has `swagger.json` and `swagger.yaml` (generated by swag init)
2. `docs.go` gets moved to `internal/api/docs.go`
3. `http-swagger` v2 is available and should be used
4. The Makefile refers to `./internal/util` which doesn't exist - needs fixing
5. The Makefile refers to `-g api.go` which doesn't exist - needs to find the right entry file

For the swagger annotations, the `swag init` command needs a "general API info" file.

I notice the handler files are missing the swagger metadata annotations like `// @title` and `// @version`, so those need to be added. Since there's no `api.go` file for `swag init -g api.go` to target, the `docs.go` is being maintained manually instead. The main tasks are updating the import in `server.go` to use `http-swagger/v2`, removing old v1 packages from go.mod, and fixing the Makefile references.

For the Makefile, I need to remove `./internal/util` from the `-d` flag since that directory doesn't exist, and create an `api.go` file in `internal/api/` with the general swagger annotations so the `-g api.go` flag has a proper entry point to target.


Check updated go.mod


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/swaggo/files/v2 v2.0.2 // indirect
	github.com/swaggo/http-swagger/v2 v2.0.2 // 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
)


Now I need to:
1. Update the import in `server.go` from `github.com/swaggo/http-swagger` to `github.com/swaggo/http-swagger/v2`
2. Create an `api.go` file in `internal/api/` with general swagger annotations (so `make swagger` works properly)
3. Fix the Makefile to remove `./internal/util` from the `-d` flag
4. Move `github.com/swaggo/http-swagger/v2` from indirect to direct in go.mod
5. Remove old v1 packages
6. Run `go mod tidy`

7. I see that docs.go already has the swagger info manually defined, but I need to add the proper swagger annotations to an `api.go` file so that `swag init` can regenerate it correctly with the API title, version, description, and contact information.

Now I'm noticing that `server.go` uses a `flagDev` global variable, so I should track down where that's actually defined.


Now I'll update the import in `server.go` and create an `api.go` with swagger general annotations, then fix the Makefile:


     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
     7→
     8→import (
     9→	"context"
    10→	"flag"
    11→	"fmt"
    12→	"os"
    13→	"os/signal"
    14→	"runtime/debug"
    15→	"sync"
    16→	"syscall"
    17→
    18→	"github.com/ClusterCockpit/cc-backend/pkg/metricstore"
    19→	ccconf "github.com/ClusterCockpit/cc-lib/v2/ccConfig"
    20→	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
    21→	"github.com/ClusterCockpit/cc-lib/v2/nats"
    22→	"github.com/ClusterCockpit/cc-lib/v2/runtime"
    23→	"github.com/ClusterCockpit/cc-metric-store/internal/api"
    24→	"github.com/ClusterCockpit/cc-metric-store/internal/config"
    25→	"github.com/google/gops/agent"
    26→)
    27→
    28→const (
    29→	envGOGC = "GOGC"
    30→)
    31→
    32→var (
    33→	date    string
    34→	commit  string
    35→	version string
    36→)
    37→
    38→var (
    39→	flagGops, flagVersion, flagDev, flagLogDateTime bool
    40→	flagConfigFile, flagLogLevel                    string
    41→)
    42→
    43→func printVersion() {
    44→	fmt.Printf("Version:\t%s\n", version)
    45→	fmt.Printf("Git hash:\t%s\n", commit)
    46→	fmt.Printf("Build time:\t%s\n", date)
    47→}
    48→
    49→func runServer(ctx context.Context) error {
    50→	var wg sync.WaitGroup
    51→
    52→	mscfg := ccconf.GetPackageConfig("metrics")
    53→	if mscfg == nil {
    54→		return fmt.Errorf("missing metrics configuration")
    55→	}
    56→	config.InitMetrics(mscfg)
    57→
    58→	mscfg = ccconf.GetPackageConfig("metric-store")
    59→	if mscfg == nil {
    60→		return fmt.Errorf("missing metricstore configuration")
    61→	}
    62→
    63→	metricstore.Init(mscfg, config.GetMetrics(), &wg)
    64→
    65→	// Set GC percent if not configured
    66→	if os.Getenv(envGOGC) == "" {
    67→		debug.SetGCPercent(15)
    68→	}
    69→
    70→	if config.Keys.BackendURL != "" {
    71→		ms := metricstore.GetMemoryStore()
    72→		ms.SetNodeProvider(api.NewBackendNodeProvider(config.Keys.BackendURL))
    73→		cclog.Infof("Node provider configured with backend URL: %s", config.Keys.BackendURL)
    74→	}
    75→
    76→	// Initialize HTTP server
    77→	srv, err := NewServer(version, commit, date)
    78→	if err != nil {
    79→		return fmt.Errorf("creating server: %w", err)
    80→	}
    81→
    82→	// Channel to collect errors from server
    83→	errChan := make(chan error, 1)
    84→
    85→	// Start HTTP server
    86→	wg.Go(func() {
    87→		if err := srv.Start(ctx); err != nil {
    88→			errChan <- err
    89→		}
    90→	})
    91→
    92→	// Handle shutdown signals
    93→	wg.Add(1)
    94→	sigs := make(chan os.Signal, 1)
    95→	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
    96→	go func() {
    97→		defer wg.Done()
    98→		select {
    99→		case <-sigs:
   100→			cclog.Info("Shutdown signal received")
   101→		case <-ctx.Done():
   102→		}
   103→
   104→		runtime.SystemdNotify(false, "Shutting down ...")
   105→		srv.Shutdown(ctx)
   106→	}()
   107→
   108→	runtime.SystemdNotify(true, "running")
   109→
   110→	// Wait for completion or errors
   111→	go func() {
   112→		wg.Wait()
   113→		close(errChan)
   114→	}()
   115→
   116→	// Wait for either server startup error or shutdown completion
   117→	if err := <-errChan; err != nil {
   118→		return err
   119→	}
   120→
   121→	cclog.Print("Graceful shutdown completed!")
   122→	return nil
   123→}
   124→
   125→func run() error {
   126→	flag.BoolVar(&flagGops, "gops", false, "Listen via github.com/google/gops/agent (for debugging)")
   127→	flag.BoolVar(&flagDev, "dev", false, "Enable development component: Swagger UI")
   128→	flag.BoolVar(&flagVersion, "version", false, "Show version information and exit")
   129→	flag.BoolVar(&flagLogDateTime, "logdate", false, "Set this flag to add date and time to log messages")
   130→	flag.StringVar(&flagConfigFile, "config", "./config.json", "Specify alternative path to `config.json`")
   131→	flag.StringVar(&flagLogLevel, "loglevel", "warn", "Sets the logging level: `[debug, info, warn (default), err, crit]`")
   132→	flag.Parse()
   133→
   134→	if flagVersion {
   135→		printVersion()
   136→		return nil
   137→	}
   138→
   139→	cclog.Init(flagLogLevel, flagLogDateTime)
   140→
   141→	if flagGops || config.Keys.Debug.EnableGops {
   142→		if err := agent.Listen(agent.Options{}); err != nil {
   143→			return fmt.Errorf("starting gops agent: %w", err)
   144→		}
   145→	}
   146→
   147→	ccconf.Init(flagConfigFile)
   148→
   149→	cfg := ccconf.GetPackageConfig("main")
   150→	if cfg == nil {
   151→		return fmt.Errorf("main configuration must be present")
   152→	}
   153→
   154→	config.Init(cfg)
   155→
   156→	natsConfig := ccconf.GetPackageConfig("nats")
   157→	if err := nats.Init(natsConfig); err != nil {
   158→		cclog.Warnf("initializing (optional) nats client: %s", err.Error())
   159→	}
   160→	nats.Connect()
   161→
   162→	ctx, cancel := context.WithCancel(context.Background())
   163→	defer cancel()
   164→
   165→	return runServer(ctx)
   166→}
   167→
   168→func main() {
   169→	if err := run(); err != nil {
   170→		cclog.Error(err.Error())
   171→		os.Exit(1)
   172→	}
   173→}
   174→

<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

import (
	"context"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"runtime/debug"
	"sync"
	"syscall"

	"github.com/ClusterCockpit/cc-backend/pkg/metricstore"
	ccconf "github.com/ClusterCockpit/cc-lib/v2/ccConfig"
	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"
	"github.com/google/gops/agent"
)

const (
	envGOGC = "GOGC"
)

var (
	date    string
	commit  string
	version string
)

var (
	flagGops, flagVersion, flagDev, flagLogDateTime bool
	flagConfigFile, flagLogLevel                    string
)

func printVersion() {
	fmt.Printf("Version:\t%s\n", version)
	fmt.Printf("Git hash:\t%s\n", commit)
	fmt.Printf("Build time:\t%s\n", date)
}

func runServer(ctx context.Context) error {
	var wg sync.WaitGroup

	mscfg := ccconf.GetPackageConfig("metrics")
	if mscfg == nil {
		return fmt.Errorf("missing metrics configuration")
	}
	config.InitMetrics(mscfg)

	mscfg = ccconf.GetPackageConfig("metric-store")
	if mscfg == nil {
		return fmt.Errorf("missing metricstore configuration")
	}

	metricstore.Init(mscfg, config.GetMetrics(), &wg)

	// Set GC percent if not configured
	if os.Getenv(envGOGC) == "" {
		debug.SetGCPercent(15)
	}

	if config.Keys.BackendURL != "" {
		ms := metricstore.GetMemoryStore()
		ms.SetNodeProvider(api.NewBackendNodeProvider(config.Keys.BackendURL))
		cclog.Infof("Node provider configured with backend URL: %s", config.Keys.BackendURL)
	}

	// Initialize HTTP server
	srv, err := NewServer(version, commit, date)
	if err != nil {
		return fmt.Errorf("creating server: %w", err)
	}

	// Channel to collect errors from server
	errChan := make(chan error, 1)

	// Start HTTP server
	wg.Go(func() {
		if err := srv.Start(ctx); err != nil {
			errChan <- err
		}
	})

	// Handle shutdown signals
	wg.Add(1)
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		defer wg.Done()
		select {
		case <-sigs:
			cclog.Info("Shutdown signal received")
		case <-ctx.Done():
		}

		runtime.SystemdNotify(false, "Shutting down ...")
		srv.Shutdown(ctx)
	}()

	runtime.SystemdNotify(true, "running")

	// Wait for completion or errors
	go func() {
		wg.Wait()
		close(errChan)
	}()

	// Wait for either server startup error or shutdown completion
	if err := <-errChan; err != nil {
		return err
	}

	cclog.Print("Graceful shutdown completed!")
	return nil
}

func run() error {
	flag.BoolVar(&flagGops, "gops", false, "Listen via github.com/google/gops/agent (for debugging)")
	flag.BoolVar(&flagDev, "dev", false, "Enable development component: Swagger UI")
	flag.BoolVar(&flagVersion, "version", false, "Show version information and exit")
	flag.BoolVar(&flagLogDateTime, "logdate", false, "Set this flag to add date and time to log messages")
	flag.StringVar(&flagConfigFile, "config", "./config.json", "Specify alternative path to `config.json`")
	flag.StringVar(&flagLogLevel, "loglevel", "warn", "Sets the logging level: `[debug, info, warn (default), err, crit]`")
	flag.Parse()

	if flagVersion {
		printVersion()
		return nil
	}

	cclog.Init(flagLogLevel, flagLogDateTime)

	if flagGops || config.Keys.Debug.EnableGops {
		if err := agent.Listen(agent.Options{}); err != nil {
			return fmt.Errorf("starting gops agent: %w", err)
		}
	}

	ccconf.Init(flagConfigFile)

	cfg := ccconf.GetPackageConfig("main")
	if cfg == nil {
		return fmt.Errorf("main configuration must be present")
	}

	config.Init(cfg)

	natsConfig := ccconf.GetPackageConfig("nats")
	if err := nats.Init(natsConfig); err != nil {
		cclog.Warnf("initializing (optional) nats client: %s", err.Error())
	}
	nats.Connect()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	return runServer(ctx)
}

func main() {
	if err := run(); err != nil {
		cclog.Error(err.Error())
		os.Exit(1)
	}
}


Now I have everything I need. Let me make the changes:

**1. Update `server.go` import to use http-swagger v2:**


ToolSearch

Question: select:Edit


Question: select:Edit


The file /Users/jan/prg/cc-metric-store/cmd/cc-metric-store/server.go has been updated successfully.

// 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()
}

-  httpSwagger "github.com/swaggo/http-swagger"

+  httpSwagger "github.com/swaggo/http-swagger/v2"


**2. Create `internal/api/api.go` with general swagger annotations:**


Question: select:Write


Question: select:Write


Now let me write the api.go file and fix the Makefile, then run go mod tidy.


Write

// 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 provides the REST API for cc-metric-store.
//
// @title       cc-metric-store REST API
// @version     1.0.0
// @description API for cc-metric-store
//
// @contact.name  ClusterCockpit Project
// @contact.url   https://clustercockpit.org
// @contact.email support@clustercockpit.org
//
// @license.name MIT License
// @license.url  https://opensource.org/licenses/MIT
//
// @host     localhost:8082
// @basePath /api/
//
// @securityDefinitions.apikey ApiKeyAuth
// @in                         header
// @name                       X-Auth-Token
package api


File created successfully at: /Users/jan/prg/cc-metric-store/internal/api/api.go

// 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 provides the REST API for cc-metric-store.
//
// @title       cc-metric-store REST API
// @version     1.0.0
// @description API for cc-metric-store
//
// @contact.name  ClusterCockpit Project
// @contact.url   https://clustercockpit.org
// @contact.email support@clustercockpit.org
//
// @license.name MIT License
// @license.url  https://opensource.org/licenses/MIT
//
// @host     localhost:8082
// @basePath /api/
//
// @securityDefinitions.apikey ApiKeyAuth
// @in                         header
// @name                       X-Auth-Token
package api


**3. Fix the Makefile swagger target:**


Edit

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

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


The file /Users/jan/prg/cc-metric-store/Makefile has been updated successfully.

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

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 ./...

-  @go run github.com/swaggo/swag/cmd/swag init -d ./internal/api,./internal/util -g api.go -o ./api

+  @go run github.com/swaggo/swag/cmd/swag init -d ./internal/api -g api.go -o ./api


**4. Run `go mod tidy` to clean up the module:**


Bash

Clean up go.mod and go.sum


**5. Verify the build and run the swagger generator:**


Verify project builds successfully


Regenerate swagger docs


Error: Exit code 2
===>  GENERATE swagger
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/gen/gen.go:20:2: missing go.sum entry for module providing package golang.org/x/text/cases (imported by github.com/swaggo/swag/gen); to add:
	go get github.com/swaggo/swag/gen@v1.16.6
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/gen/gen.go:21:2: missing go.sum entry for module providing package golang.org/x/text/language (imported by github.com/swaggo/swag/gen); to add:
	go get github.com/swaggo/swag/gen@v1.16.6
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/gen/gen.go:22:2: missing go.sum entry for module providing package sigs.k8s.io/yaml (imported by github.com/swaggo/swag/gen); to add:
	go get github.com/swaggo/swag/gen@v1.16.6
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/cmd/swag/main.go:10:2: missing go.sum entry for module providing package github.com/urfave/cli/v2 (imported by github.com/swaggo/swag/cmd/swag); to add:
	go get github.com/swaggo/swag/cmd/swag@v1.16.6
make: *** [swagger] Error 1

===>  GENERATE swagger
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/gen/gen.go:20:2: missing go.sum entry for module providing package golang.org/x/text/cases (imported by github.com/swaggo/swag/gen); to add:
	go get github.com/swaggo/swag/gen@v1.16.6
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/gen/gen.go:21:2: missing go.sum entry for module providing package golang.org/x/text/language (imported by github.com/swaggo/swag/gen); to add:
	go get github.com/swaggo/swag/gen@v1.16.6
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/gen/gen.go:22:2: missing go.sum entry for module providing package sigs.k8s.io/yaml (imported by github.com/swaggo/swag/gen); to add:
	go get github.com/swaggo/swag/gen@v1.16.6
../../go/pkg/mod/github.com/swaggo/swag@v1.16.6/cmd/swag/main.go:10:2: missing go.sum entry for module providing package github.com/urfave/cli/v2 (imported by github.com/swaggo/swag/cmd/swag); to add:
	go get github.com/swaggo/swag/cmd/swag@v1.16.6
make: *** [swagger] Error 1


go get github.com/swaggo/swag/cmd/swag@v1.16.6 2>&1 && go mod tidy 2>&1

Add missing go.sum entries for swag tool dependencies


Regenerate swagger docs


The issue is that `go run github.com/swaggo/swag/cmd/swag` needs the tool's dependencies in the go.sum. The typical approach is to use `go tool` or a separate tool module. Let me try using GONOSUMCHECK or use `GOFLAGS=-mod=mod` when running.

Actually, the issue is that `go mod tidy` removes entries that aren't needed for build, but `go run` for tools needs them in go.sum. The solution is to add a `tools.go` file or use the `go tool` mechanism.

Actually, the better solution is to use a `tools.go` file with build tag `tools` to include the swag command as an import so `go mod tidy` keeps the necessary dependencies.

I could also modify the Makefile to pass `GOFLAGS=-mod=mod` or pre-install swag with `go install`, but the cleanest approach is the `tools.go` file with the `//go:build tools` tag—it's the standard Go pattern for managing tool dependencies.


The `go run` tool needs its dependencies in `go.sum`. The standard fix is a `tools.go` file:


Write

//go:build tools

package tools

import _ "github.com/swaggo/swag/cmd/swag"


File created successfully at: /Users/jan/prg/cc-metric-store/tools/tools.go

//go:build tools

package tools

import _ "github.com/swaggo/swag/cmd/swag"


Bash

Re-run go mod tidy to include tool dependencies in go.sum


GOFLAGS=-mod=mod go get github.com/swaggo/swag/cmd/swag@v1.16.6 github.com/swaggo/swag/gen@v1.16.6 2>&1 && go mod tidy 2>&1

Fetch swag tool deps and tidy


Regenerate swagger docs


Error: Exit code 2
===>  GENERATE swagger
2026/03/06 12:03:45 Generate swagger docs....
2026/03/06 12:03:45 Generate general API Info, search dir:./internal/api
2026/03/06 12:03:45 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     api.ApiQueryRequest  true "API query payload object"': cannot find type definition: api.ApiQueryRequest
exit status 1
make: *** [swagger] Error 1

===>  GENERATE swagger
2026/03/06 12:03:45 Generate swagger docs....
2026/03/06 12:03:45 Generate general API Info, search dir:./internal/api
2026/03/06 12:03:45 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     api.ApiQueryRequest  true "API query payload object"': cannot find type definition: api.ApiQueryRequest
exit status 1
make: *** [swagger] Error 1


The swagger comment in `metricstore.go` references `api.ApiQueryRequest` but since it's in the same package, it should just be `ApiQueryRequest`. Let me fix the swagger annotations in `metricstore.go`.

Looking at line 156:
```go
// @param       request body     api.ApiQueryRequest  true "API query payload object"
// @success     200            {object} api.ApiQueryResponse  "API query response object"
```

These should be without the `api.` prefix since they're in the same package.


The swagger annotations use the wrong type reference prefix. Fix them in `metricstore.go`:


Edit

// @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"

// @param       request body     ApiQueryRequest  true "API query payload object"
// @success     200            {object} ApiQueryResponse  "API query response object"
// @failure     400            {object} ErrorResponse       "Bad Request"
// @failure     401   		   {object} ErrorResponse       "Unauthorized"
// @failure     403            {object} ErrorResponse       "Forbidden"
// @failure     500            {object} ErrorResponse       "Internal Server Error"


The file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go has been updated successfully.

// @param       request body     ApiQueryRequest  true "API query payload object"
// @success     200            {object} ApiQueryResponse  "API query response object"
// @failure     400            {object} ErrorResponse       "Bad Request"
// @failure     401   		   {object} ErrorResponse       "Unauthorized"
// @failure     403            {object} ErrorResponse       "Forbidden"
// @failure     500            {object} ErrorResponse       "Internal Server Error"

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

-// @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"

+// @param       request body     ApiQueryRequest  true "API query payload object"

+// @success     200            {object} ApiQueryResponse  "API query response object"

+// @failure     400            {object} ErrorResponse       "Bad Request"

+// @failure     401   		   {object} ErrorResponse       "Unauthorized"

+// @failure     403            {object} ErrorResponse       "Forbidden"

+// @failure     500            {object} ErrorResponse       "Internal Server Error"


Bash

grep -n 'api\.ErrorResponse\|api\.Api' /Users/jan/prg/cc-metric-store/internal/api/metricstore.go

Check for remaining api. prefixed swagger type refs


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


The file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go has been updated. All occurrences of 'api.ErrorResponse' were successfully replaced with 'ErrorResponse'.

// 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     ApiQueryRequest  true "API query payload object"
// @success     200            {object} ApiQueryResponse  "API query response object"
// @failure     400            {object} ErrorResponse       "Bad Request"
// @failure     401   		   {object} ErrorResponse       "Unauthorized"
// @failure     403            {object} ErrorResponse       "Forbidden"
// @failure     500            {object} 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 ""
}

// @success     200            {string} string  "ok"

-// @failure     401            {object} api.ErrorResponse       "Unauthorized"

+// @failure     401            {object} ErrorResponse       "Unauthorized"

-// @failure     400            {object} api.ErrorResponse       "Bad Request"

-// @failure     403            {object} api.ErrorResponse       "Forbidden"

-// @failure     500            {object} api.ErrorResponse       "Internal Server Error"

+// @failure     400            {object} ErrorResponse       "Bad Request"

+// @failure     403            {object} ErrorResponse       "Forbidden"

+// @failure     500            {object} ErrorResponse       "Internal Server Error"


Regenerate swagger docs


Error: Exit code 2
===>  GENERATE swagger
2026/03/06 12:04:49 Generate swagger docs....
2026/03/06 12:04:49 Generate general API Info, search dir:./internal/api
2026/03/06 12:04:49 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1
make: *** [swagger] Error 1

===>  GENERATE swagger
2026/03/06 12:04:49 Generate swagger docs....
2026/03/06 12:04:49 Generate general API Info, search dir:./internal/api
2026/03/06 12:04:49 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1
make: *** [swagger] Error 1


The issue is that swag can't find `ApiQueryRequest` type. This is odd since it's in the same package. The swag tool should be able to find it. Maybe the issue is that swag uses the package import path when running `go run`. Let me try using `--parseDependency` flag or check if there's a parsing issue.

Wait, looking at the docs.go, the types are referenced as `api.ApiQueryRequest` (with `api.` prefix in the docs). This is because when swag generates docs, it adds the package name prefix. The swagger annotations should use the full type name with package prefix.

Actually, looking at the swag documentation more carefully - in swagger annotations within the same package, types should be referenced without the package prefix, but swag might have some quirk where it requires the full package path.

Let me try different flags like `--parseInternal` or `--packageName` to see if that helps swag locate the type definitions. The core issue seems to be that swag's comment parser can't resolve `ApiQueryRequest` even though it's defined in the same `api` package, so maybe I need to adjust how I'm invoking the tool or referencing the types in the annotations.

Wait, I'm noticing something in the docs.go output - it shows `"api.ApiQueryRequest"` in the definitions, which suggests swag might actually require the fully qualified package name in the comments for non-main packages. Let me try updating the comment to use `api.ApiQueryRequest` instead.


go run github.com/swaggo/swag/cmd/swag init -d ./internal/api -g api.go -o ./api --help 2>&1 | grep -i parse

Check available swag parsing flags


   --dir value, -d value                      Directories you want to parse,comma separated and general-info file must be in the first one (default: "./")
   --parseVendor                              Parse go files in 'vendor' folder, disabled by default (default: false)
   --parseDependencyLevel value, --pdl value  Parse go files inside dependency folder, 0 disabled, 1 only parse models, 2 only parse operations, 3 parse all (default: 0)
   --parseDependency, --pd                    Parse go files inside dependency folder, disabled by default (default: false)
   --markdownFiles value, --md value          Parse folder containing markdown files to use as description, disabled by default
   --codeExampleFiles value, --cef value      Parse folder containing code example files to use for the x-codeSamples extension, disabled by default
   --parseInternal                            Parse go files in internal packages, disabled by default (default: false)
   --parseDepth value                         Dependency parse depth (default: 100)
   --parseGoList                              Parse dependency via 'go list' (default: true)
   --parseExtension value                     Parse only those operations that match given extension
   --packagePrefix value                      Parse only packages whose import path match the given prefix, comma separated
   --parseFuncBody                            Parse API info within body of functions in go files, disabled by default (default: false)


go run github.com/swaggo/swag/cmd/swag init -d ./internal/api -g api.go -o ./api --parseInternal 2>&1

Run swag with parseInternal flag


Error: Exit code 1
2026/03/06 12:05:17 Generate swagger docs....
2026/03/06 12:05:17 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:17 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1

2026/03/06 12:05:17 Generate swagger docs....
2026/03/06 12:05:17 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:17 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1


Let me try with the package-qualified type name (which is what the original code had):


go run github.com/swaggo/swag/cmd/swag init -d ./internal/api -g api.go -o ./api --parseDependency 2>&1

Run swag with parseDependency flag


Error: Exit code 1
2026/03/06 12:05:20 Generate swagger docs....
2026/03/06 12:05:20 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:21 warning: failed to evaluate const CheckpointFilePerms at /Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-backend@v1.5.0/pkg/metricstore/checkpoint.go:59:2, strconv.ParseUint: parsing "o644": invalid syntax
2026/03/06 12:05:21 warning: failed to evaluate const CheckpointDirPerms at /Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-backend@v1.5.0/pkg/metricstore/checkpoint.go:60:2, strconv.ParseUint: parsing "o755": invalid syntax
2026/03/06 12:05:21 atomic.align64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uintptr TypeSpecDef is nil
2026/03/06 12:05:21 atomic.noCopy TypeSpecDef is nil
2026/03/06 12:05:21 label.Label TypeSpecDef is nil
2026/03/06 12:05:21 cpu.CacheLinePad TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Bool TypeSpecDef is nil
2026/03/06 12:05:21 cpu.option TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Pointer TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uintptr TypeSpecDef is nil
2026/03/06 12:05:21 atomic.noCopy TypeSpecDef is nil
2026/03/06 12:05:21 label.Label TypeSpecDef is nil
2026/03/06 12:05:21 cpu.CacheLinePad TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Bool TypeSpecDef is nil
2026/03/06 12:05:21 cpu.option TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Pointer TypeSpecDef is nil
2026/03/06 12:05:21 atomic.align64 TypeSpecDef is nil
2026/03/06 12:05:21 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1

2026/03/06 12:05:20 Generate swagger docs....
2026/03/06 12:05:20 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:21 warning: failed to evaluate const CheckpointFilePerms at /Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-backend@v1.5.0/pkg/metricstore/checkpoint.go:59:2, strconv.ParseUint: parsing "o644": invalid syntax
2026/03/06 12:05:21 warning: failed to evaluate const CheckpointDirPerms at /Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-backend@v1.5.0/pkg/metricstore/checkpoint.go:60:2, strconv.ParseUint: parsing "o755": invalid syntax
2026/03/06 12:05:21 atomic.align64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uintptr TypeSpecDef is nil
2026/03/06 12:05:21 atomic.noCopy TypeSpecDef is nil
2026/03/06 12:05:21 label.Label TypeSpecDef is nil
2026/03/06 12:05:21 cpu.CacheLinePad TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Bool TypeSpecDef is nil
2026/03/06 12:05:21 cpu.option TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Pointer TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int32 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uint64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Uintptr TypeSpecDef is nil
2026/03/06 12:05:21 atomic.noCopy TypeSpecDef is nil
2026/03/06 12:05:21 label.Label TypeSpecDef is nil
2026/03/06 12:05:21 cpu.CacheLinePad TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Int64 TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Bool TypeSpecDef is nil
2026/03/06 12:05:21 cpu.option TypeSpecDef is nil
2026/03/06 12:05:21 atomic.Pointer TypeSpecDef is nil
2026/03/06 12:05:21 atomic.align64 TypeSpecDef is nil
2026/03/06 12:05:21 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1


The swag tool can't find `ApiQueryRequest` even though it's in the same package. This is a known issue with swag - for non-main packages, types need to be referenced with the package name prefix. Let me revert to using `api.ApiQueryRequest` and `api.ApiQueryResponse`.

Actually, looking at many swag examples and the old docs.go output (which had `api.ApiQueryRequest`), the annotations DO need the package prefix when the type is in a non-main package. Let me revert the changes:

Actually wait - looking at this more carefully, the swag tool was complaining about `api.ApiQueryRequest` earlier, and now it's complaining about `ApiQueryRequest`.

The original error shows that even with the package prefix, swag couldn't find the type definition. The problem might be that `swag init` from the module root doesn't automatically discover types in the `./internal/api` directory the way it should.

I'm wondering if the `-g api.go` flag is changing how swag searches for types, or if there's something about the existing `docs.go` file in that same directory that's interfering with the parsing. Let me look more closely at how swag actually resolves type references when it encounters them in comments. when running `swag init -d ./internal/api`, it should parse all Go files in that directory. The types `ApiQueryRequest`, `ApiQueryResponse` etc. are defined in `metricstore.go` in the same directory. This should work.

Wait, maybe the issue is that the code `go run github.com/swaggo/swag/cmd/swag` needs to be compiled with the right module context. Let me try running with `--output-types go,json,yaml` or check if there's a parsing issue with the file.

Actually, let me look at the swag documentation again. I think the issue might be that for `@param` with `body` type, swag uses a different lookup mechanism. Let me look at how similar projects use swag...

Actually, I think the issue might be that the `-g api.go` flag expects the main file with the general info annotations. Since the types are in the `api` package rather than a main package, swag might need a different approach. Let me try removing the `-g api.go` flag and see if swag can discover everything on its own.


go run github.com/swaggo/swag/cmd/swag init -d ./internal/api -o ./api 2>&1

Try swag without -g flag to debug type resolution


Error: Exit code 1
2026/03/06 12:05:40 Generate swagger docs....
2026/03/06 12:05:40 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:40 cannot parse source files /Users/jan/prg/cc-metric-store/internal/api/main.go: open /Users/jan/prg/cc-metric-store/internal/api/main.go: no such file or directory
exit status 1

2026/03/06 12:05:40 Generate swagger docs....
2026/03/06 12:05:40 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:40 cannot parse source files /Users/jan/prg/cc-metric-store/internal/api/main.go: open /Users/jan/prg/cc-metric-store/internal/api/main.go: no such file or directory
exit status 1


go run github.com/swaggo/swag/cmd/swag init -d ./internal/api -g api.go -o ./api --instanceName swagger 2>&1

Try swag with instanceName


Error: Exit code 1
2026/03/06 12:05:44 Generate swagger docs....
2026/03/06 12:05:44 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:44 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1

2026/03/06 12:05:44 Generate swagger docs....
2026/03/06 12:05:44 Generate general API Info, search dir:./internal/api
2026/03/06 12:05:44 ParseComment error in file /Users/jan/prg/cc-metric-store/internal/api/metricstore.go for comment: '// @param       request body     ApiQueryRequest  true "API query payload object"': cannot find type definition: ApiQueryRequest
exit status 1


Let me look at what's happening. Swag is failing to find `ApiQueryRequest`. The type IS defined in `metricstore.go` in the same directory. Let me check if the issue is with the existing `docs.go` or any other file.

Actually wait, maybe the issue is that swag is doing its own parsing and somehow the `docs.go` file (which has `package api`) is confusing things, or it doesn't parse all files before resolving types.

Let me look at the actual type definition line in metricstore.go:

```go
type APIQueryRequest struct {
```

But the annotation says:

`ApiQueryRequest`

Note: the type is named `APIQueryRequest` (uppercase API) but the annotation uses `ApiQueryRequest`! The capitalization is different — the struct is `APIQueryRequest` but the annotation references `ApiQueryRequest`. That's the mismatch causing swag to fail.


The issue is likely a type name mismatch. Let me check the exact struct names:


grep -n '^type API' /Users/jan/prg/cc-metric-store/internal/api/metricstore.go

List API type definitions


52:type APIMetricData struct {
120:type APIQueryRequest struct {
131:type APIQueryResponse struct {
136:type APIQuery struct {


The types are `APIQueryRequest`, `APIQueryResponse`, `APIMetricData`, `APIQuery`. But the annotations reference `ApiQueryRequest`, `ApiQueryResponse` (with lowercase `pi`). Swag should handle this since it normalizes type names, but let me fix the annotations to match the actual type names. Swag should handle the exact Go type names.

Actually, I recall that swag v1.16.x does support resolving type names by their actual Go names. The annotations should use the exact Go type name. Let me update them:
- `ApiQueryRequest` → `APIQueryRequest`
- `ApiQueryResponse` → `APIQueryResponse`

- The docs.go file is auto-generated, so I don't need to manually update those references.


The annotations use wrong casing (`ApiQueryRequest` vs `APIQueryRequest`). Fix to match actual type names:


Edit

// @param       request body     ApiQueryRequest  true "API query payload object"
// @success     200            {object} ApiQueryResponse  "API query response object"

// @param       request body     APIQueryRequest  true "API query payload object"
// @success     200            {object} APIQueryResponse  "API query response object"