Update cc-backend to latest master HEAD


The user wants to update the cc-backend dependency to the latest master HEAD. Let me first check the current go.mod to see the current version of cc-backend.


     1→module github.com/ClusterCockpit/cc-metric-store
     2→
     3→go 1.25.0
     4→
     5→require (
     6→	github.com/ClusterCockpit/cc-backend v1.4.5-0.20260224192951-a6e23dd52e80
     7→	github.com/ClusterCockpit/cc-lib/v2 v2.7.0
     8→	github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0
     9→	github.com/golang-jwt/jwt/v4 v4.5.0
    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/aws/aws-sdk-go-v2 v1.41.1 // indirect
    19→	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
    20→	github.com/aws/aws-sdk-go-v2/config v1.32.8 // indirect
    21→	github.com/aws/aws-sdk-go-v2/credentials v1.19.8 // indirect
    22→	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
    23→	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
    24→	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
    25→	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
    26→	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
    27→	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
    28→	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
    29→	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
    30→	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
    31→	github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect
    32→	github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
    33→	github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
    34→	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
    35→	github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
    36→	github.com/aws/smithy-go v1.24.0 // indirect
    37→	github.com/fsnotify/fsnotify v1.9.0 // indirect
    38→	github.com/go-openapi/jsonpointer v0.22.4 // indirect
    39→	github.com/go-openapi/jsonreference v0.21.4 // indirect
    40→	github.com/go-openapi/spec v0.22.3 // indirect
    41→	github.com/go-openapi/swag/conv v0.25.4 // indirect
    42→	github.com/go-openapi/swag/jsonname v0.25.4 // indirect
    43→	github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
    44→	github.com/go-openapi/swag/loading v0.25.4 // indirect
    45→	github.com/go-openapi/swag/stringutils v0.25.4 // indirect
    46→	github.com/go-openapi/swag/typeutils v0.25.4 // indirect
    47→	github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
    48→	github.com/golang/snappy v1.0.0 // indirect
    49→	github.com/influxdata/line-protocol/v2 v2.2.1 // indirect
    50→	github.com/klauspost/compress v1.18.4 // indirect
    51→	github.com/linkedin/goavro/v2 v2.15.0 // indirect
    52→	github.com/mattn/go-sqlite3 v1.14.34 // indirect
    53→	github.com/nats-io/nats.go v1.49.0 // indirect
    54→	github.com/nats-io/nkeys v0.4.15 // indirect
    55→	github.com/nats-io/nuid v1.0.1 // indirect
    56→	github.com/swaggo/files v1.0.1 // indirect
    57→	go.yaml.in/yaml/v3 v3.0.4 // indirect
    58→	golang.org/x/crypto v0.48.0 // indirect
    59→	golang.org/x/mod v0.33.0 // indirect
    60→	golang.org/x/net v0.51.0 // indirect
    61→	golang.org/x/sync v0.19.0 // indirect
    62→	golang.org/x/sys v0.41.0 // indirect
    63→	golang.org/x/tools v0.42.0 // indirect
    64→	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
    65→)
    66→

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

go 1.25.0

require (
	github.com/ClusterCockpit/cc-backend v1.4.5-0.20260224192951-a6e23dd52e80
	github.com/ClusterCockpit/cc-lib/v2 v2.7.0
	github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0
	github.com/golang-jwt/jwt/v4 v4.5.0
	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/aws/aws-sdk-go-v2 v1.41.1 // indirect
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
	github.com/aws/aws-sdk-go-v2/config v1.32.8 // indirect
	github.com/aws/aws-sdk-go-v2/credentials v1.19.8 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
	github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 // indirect
	github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
	github.com/aws/smithy-go v1.24.0 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/go-openapi/jsonpointer v0.22.4 // indirect
	github.com/go-openapi/jsonreference v0.21.4 // indirect
	github.com/go-openapi/spec v0.22.3 // indirect
	github.com/go-openapi/swag/conv v0.25.4 // indirect
	github.com/go-openapi/swag/jsonname v0.25.4 // indirect
	github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
	github.com/go-openapi/swag/loading v0.25.4 // indirect
	github.com/go-openapi/swag/stringutils v0.25.4 // indirect
	github.com/go-openapi/swag/typeutils v0.25.4 // indirect
	github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
	github.com/golang/snappy v1.0.0 // indirect
	github.com/influxdata/line-protocol/v2 v2.2.1 // indirect
	github.com/klauspost/compress v1.18.4 // indirect
	github.com/linkedin/goavro/v2 v2.15.0 // 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/swaggo/files v1.0.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
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)


Bash

Update cc-backend to latest master HEAD


Output: go: downloading github.com/ClusterCockpit/cc-backend v1.4.5-0.20260303062012-8b0f63b1f849

go: downloading github.com/ClusterCockpit/cc-backend v1.4.5-0.20260303062012-8b0f63b1f849


Output: go: downloading github.com/ClusterCockpit/cc-backend v1.4.5-0.20260303062012-8b0f63b1f849


Output: go: downloading github.com/ClusterCockpit/cc-backend v1.4.5-0.20260303062012-8b0f63b1f849


go: downloading github.com/ClusterCockpit/cc-backend v1.4.5-0.20260303062012-8b0f63b1f849
go: upgraded github.com/ClusterCockpit/cc-backend v1.4.5-0.20260224192951-a6e23dd52e80 => v1.4.5-0.20260303062012-8b0f63b1f849


Tidy module dependencies


Now let me check if the build still works, since the diagnostics showed compile errors:


Check if the project builds


Error: Exit code 1
# github.com/ClusterCockpit/cc-metric-store/internal/api
internal/api/metricstore.go:363:13: undefined: queryParam

# github.com/ClusterCockpit/cc-metric-store/internal/api
internal/api/metricstore.go:363:13: undefined: queryParam


There are compile errors. Let me look at the problematic file to understand the issue.


There are build errors. Let me investigate and fix them.


   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"

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


No files found


internal/api/metricstore.go:363:	cluster := queryParam(r.URL.RawQuery, "cluster")


The `queryParam` function doesn't exist anywhere. Let me also check the `DecodeLine` signature and the line-protocol import issue.


     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"

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


No matches found


grep -r "func DecodeLine" "$(go env GOMODCACHE)/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/" 2>/dev/null | head -5

Find DecodeLine in cc-backend module


    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 {

	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 {


ls "$(go env GOMODCACHE)/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/metricstore/"

List metricstore package files


Error: Exit code 2
"/Users/jan/go/pkg/mod/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/metricstore/": No such file or directory (os error 2)

"/Users/jan/go/pkg/mod/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/metricstore/": No such file or directory (os error 2)


find "$(go env GOMODCACHE)/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/" -type d | head -20

Find directories in cc-backend pkg


find: /Users/jan/go/pkg/mod/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/: No such file or directory


ls "$(go env GOMODCACHE)/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/"

List cc-backend root directory


Error: Exit code 2
"/Users/jan/go/pkg/mod/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/": No such file or directory (os error 2)

"/Users/jan/go/pkg/mod/github.com/ClusterCockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/": No such file or directory (os error 2)


ls "$(go env GOMODCACHE)/github.com/!C*luster!C*ockpit/" 2>/dev/null; ls "$(go env GOMODCACHE)/github.com/" | grep -i cluster

Find ClusterCockpit in module cache


!cluster!cockpit


ls "$(go env GOMODCACHE)/github.com/!cluster!cockpit/"

List ClusterCockpit modules


cc-backend@v1.4.0
cc-backend@v1.4.1-0.20260123065417-b7df4f7ccade
cc-backend@v1.4.4
cc-backend@v1.4.4-0.20260126082752-084d00cb0d0c
cc-backend@v1.4.4-0.20260128102127-0d857b49a256
cc-backend@v1.4.5-0.20260224192951-a6e23dd52e80
cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849
cc-lib
cc-lib@v0.10.0
cc-lib@v0.10.1
cc-lib@v0.10.2
cc-lib@v0.11.0
cc-lib@v1.0.0
cc-lib@v1.0.1
cc-lib@v1.0.2
cc-line-protocol
cc-node-controller@v0.1.0
cc-units@v0.4.0


find "$(go env GOMODCACHE)/github.com/!cluster!cockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/metricstore/" -name "*.go" | head -20

Find metricstore Go files in new cc-backend


grep -n "func DecodeLine\|func.*DecodeLine" "$(go env GOMODCACHE)/github.com/!cluster!cockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/metricstore/lineprotocol.go"

Check DecodeLine signature in new cc-backend


194:func DecodeLine(dec *lineprotocol.Decoder,


   190→//
   191→// When the checkpoint format is "wal" each successfully decoded sample is also
   192→// sent to WALMessages so the WAL staging goroutine can persist it durably
   193→// before the next binary snapshot.
   194→func DecodeLine(dec *lineprotocol.Decoder,
   195→	ms *MemoryStore,
   196→	clusterDefault string,
   197→) error {
   198→	// Reduce allocations in loop:
   199→	t := time.Now()

//
// When the checkpoint format is "wal" each successfully decoded sample is also
// sent to WALMessages so the WAL staging goroutine can persist it durably
// before the next binary snapshot.
func DecodeLine(dec *lineprotocol.Decoder,
	ms *MemoryStore,
	clusterDefault string,
) error {
	// Reduce allocations in loop:
	t := time.Now()


head -10 "$(go env GOMODCACHE)/github.com/!cluster!cockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/metricstore/lineprotocol.go"

Check lineprotocol import in new cc-backend


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

// This file implements ingestion of InfluxDB line-protocol metric data received
// over NATS. Each line encodes one metric sample with the following structure:
//
//	<measurement>[,cluster=<c>][,hostname=<h>][,type=<t>][,type-id=<id>][,subtype=<s>][,stype-id=<id>] value=<v> [<timestamp>]
//


grep "lineprotocol" "$(go env GOMODCACHE)/github.com/!cluster!cockpit/cc-backend@v1.4.5-0.20260303062012-8b0f63b1f849/pkg/metricstore/lineprotocol.go" | head -5

Check which lineprotocol package cc-backend uses


	"github.com/ClusterCockpit/cc-line-protocol/v2/lineprotocol"
						dec := lineprotocol.NewDecoderWithBytes(m)
				dec := lineprotocol.NewDecoderWithBytes(data)
func DecodeLine(dec *lineprotocol.Decoder,
			if val.Kind() == lineprotocol.Float {


Good — both the new cc-backend and this project use `github.com/ClusterCockpit/cc-line-protocol/v2/lineprotocol`, so the `DecodeLine` type mismatch in the diagnostics was from before the update. Now I need to fix the `queryParam` issue. Let me check what the old cc-backend version had.


grep -rn "queryParam" "$(go env GOMODCACHE)/github.com/!cluster!cockpit/cc-backend@v1.4.5-0.20260224192951-a6e23dd52e80/pkg/metricstore/"

Check if queryParam existed in old cc-backend


It wasn't in the old version either. Let me check git log to understand how `queryParam` got introduced.


git log --oneline -10 -- internal/api/metricstore.go

Check recent changes to metricstore.go


3cc9420 Merge branch 'dev' of github.com:ClusterCockpit/cc-metric-store into dev
b764120 Integrate entire. Upgrade dependencies
4de275a fix error conditional
ef3504d Update to the error handling
be6d4be Fix healthcheck api
6aca448 Port to newest cclib. Use metricstore as library.


git show b764120 -- internal/api/metricstore.go | head -80

Check what changed in the most recent commit


commit b764120501c303c64abe57d7fc892089929d8fea
Author: Jan Eitzinger <jan@moebiusband.org>
Date:   Tue Mar 3 07:13:15 2026 +0100

    Integrate entire. Upgrade dependencies

diff --git a/internal/api/metricstore.go b/internal/api/metricstore.go
index e043079..d842256 100644
--- a/internal/api/metricstore.go
+++ b/internal/api/metricstore.go
@@ -10,7 +10,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io"
 	"log"
 	"math"
 	"net/http"
@@ -22,7 +21,7 @@ import (
 	"github.com/ClusterCockpit/cc-lib/v2/schema"
 	"github.com/ClusterCockpit/cc-lib/v2/util"

-	"github.com/influxdata/line-protocol/v2/lineprotocol"
+	"github.com/ClusterCockpit/cc-line-protocol/v2/lineprotocol"
 )

 // ErrorResponse model
@@ -356,16 +355,17 @@ func freeMetrics(rw http.ResponseWriter, r *http.Request) {
 // @security    ApiKeyAuth
 // @router      /write/ [post]
 func writeMetrics(rw http.ResponseWriter, r *http.Request) {
-	bytes, err := io.ReadAll(r.Body)
 	rw.Header().Add("Content-Type", "application/json")
-	if err != nil {
-		handleError(err, http.StatusInternalServerError, rw)
-		return
-	}

+	// 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.NewDecoderWithBytes(bytes)
-	if err := metricstore.DecodeLine(dec, ms, r.URL.Query().Get("cluster")); err != nil {
+	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


So the commit `b764120` introduced the `queryParam` function call but didn't include the function definition. This was a bug in the recent commit. I need to either:

1. Implement the `queryParam` function, or
2. Revert to `r.URL.Query().Get("cluster")`

The `queryParam` function is meant to extract a query parameter without allocating a `url.Values` map. Let me check if `lineprotocol.NewDecoder` exists in cc-line-protocol.


The recent commit `b764120` introduced `queryParam` but never defined it. Let me also check if `NewDecoder` (vs `NewDecoderWithBytes`) exists in cc-line-protocol.


grep -n "func NewDecoder" "$(go env GOMODCACHE)/github.com/!cluster!cockpit/cc-line-protocol/" -r

Check available constructors in cc-line-protocol


/Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-line-protocol/v2@v2.3.0/lineprotocol/decoder.go:106:func NewDecoderWithBytes(buf []byte) *Decoder {
/Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-line-protocol/v2@v2.3.0/lineprotocol/decoder.go:116:func NewDecoder(r io.Reader) *Decoder {
/Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-line-protocol/v2@v2.4.0/lineprotocol/decoder.go:106:func NewDecoderWithBytes(buf []byte) *Decoder {
/Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-line-protocol/v2@v2.4.0/lineprotocol/decoder.go:116:func NewDecoder(r io.Reader) *Decoder {


Good, `NewDecoder(io.Reader)` exists. The only issue is the missing `queryParam` function. I'll implement it — it's a simple helper to avoid allocating a `url.Values` map, as the comment describes. Alternatively, I can just use the standard `r.URL.Query().Get("cluster")` which is simpler and correct.

Given the comment says "without allocating a url.Values map", let me implement a simple `queryParam` helper:


   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

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


Let me find a good place to add the helper function. I'll put it near the end of the file or near the write function.


Get file length


448 /Users/jan/prg/ClusterCockpit/cc-metric-store/internal/api/metricstore.go


   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→

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


Edit

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

	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 i := strings.IndexByte(rawQuery, '&'); i >= 0 {
			kv, rawQuery = rawQuery[:i], rawQuery[i+1:]
		} else {
			kv, rawQuery = rawQuery, ""
		}
		if eq := strings.IndexByte(kv, '='); eq >= 0 {
			if kv[:eq] == key {
				return kv[eq+1:]
			}
		} else if kv == key {
			return ""
		}
	}
	return ""
}


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

	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 i := strings.IndexByte(rawQuery, '&'); i >= 0 {
			kv, rawQuery = rawQuery[:i], rawQuery[i+1:]
		} else {
			kv, rawQuery = rawQuery, ""
		}
		if eq := strings.IndexByte(kv, '='); eq >= 0 {
			if kv[:eq] == key {
				return kv[eq+1:]
			}
		} else if kv == key {
			return ""
		}
	}
	return ""
}

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

+    if i := strings.IndexByte(rawQuery, '&'); i >= 0 {

+      kv, rawQuery = rawQuery[:i], rawQuery[i+1:]

+    if eq := strings.IndexByte(kv, '='); eq >= 0 {


Bash

Verify the project builds


Build succeeds. Let me also run the tests:


Run go vet


Done. Here's a summary of what was done:

1. **Updated cc-backend** from `v1.4.5-0.20260224192951-a6e23dd52e80` to `v1.4.5-0.20260303062012-8b0f63b1f849` (latest master HEAD)
2. **Fixed missing `queryParam` function** — the recent commit `b764120` introduced a call to `queryParam()` in `writeMetrics()` but never defined it. Added the implementation at `internal/api/metricstore.go:451` — a lightweight helper that extracts a single query parameter from the raw query string without allocating a `url.Values` map.