diff --git a/.circleci/config.yml b/.circleci/config.yml index 886357aa81d2b..8e0491ed72d41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -115,7 +115,7 @@ jobs: - run: 'make check-deps' - run: name: "Install golangci-lint" - command: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + command: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2 - run: name: "golangci-lint/Linux" # There are only 4 vCPUs available for this executor, so use only 4 instead of the default number @@ -129,7 +129,7 @@ jobs: - check-changed-files-or-halt - run: name: "Install golangci-lint" - command: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + command: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2 - run: name: "golangci-lint/macOS" # There are only 4 vCPUs available for this executor, so use only 4 instead of the default number @@ -143,7 +143,7 @@ jobs: - check-changed-files-or-halt - run: name: "Install golangci-lint" - command: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + command: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2 - run: name: "golangci-lint/Windows" # There are only 4 vCPUs available for this executor, so use only 4 instead of the default number diff --git a/.golangci.yml b/.golangci.yml index 55bff1726d208..45c7468adc84c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,12 +2,15 @@ version: "2" linters: # Default set of linters. - # The value can be: `standard`, `all`, `none`, or `fast`. + # The value can be: + # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default + # - `all`: enables all linters by default. + # - `none`: disables all linters by default. + # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`). # Default: standard default: none # Enable specific linter. - # https://golangci-lint.run/usage/linters/#enabled-by-default enable: - asasalint - asciicheck @@ -45,6 +48,7 @@ linters: - unused - usetesting + # All available settings of specific linters. settings: depguard: # Rules to apply. @@ -405,6 +409,7 @@ linters: staticcheck: # SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks # Example (to disable some checks): [ "all", "-SA1000", "-SA1001"] + # Run `GL_DEBUG=staticcheck golangci-lint run --enable=staticcheck` to see all available checks and enabled by config checks. # Default: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] checks: - all @@ -517,6 +522,10 @@ linters: - path: migrations/.*\.go$ text: don't use MixedCaps in package name + # revive:var-naming: Exclude check for package names that conflict with standard library package names (e.g., "net", "json", "http", "os") + - path: (.*)\.go$ + text: conflict with Go standard library package names + # revive:exported - path: (.+)\.go$ text: exported method .*\.(Init |SampleConfig |Gather |Start |Stop |GetState |SetState |SetParser |SetParserFunc |SetTranslator |Probe |Add |Push |Reset |Serialize |SerializeBatch |Get |Set |List |GetResolver |Apply |SetSerializer )should have comment or be unexported diff --git a/Makefile b/Makefile index f8ffabb1ec294..b76fe89c2916f 100644 --- a/Makefile +++ b/Makefile @@ -180,7 +180,7 @@ vet: .PHONY: lint-install lint-install: @echo "Installing golangci-lint" - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2 @echo "Installing markdownlint" npm install -g markdownlint-cli diff --git a/plugins/common/snmp/translator_netsnmp.go b/plugins/common/snmp/translator_netsnmp.go index 4f20dbd837713..a62c19523a984 100644 --- a/plugins/common/snmp/translator_netsnmp.go +++ b/plugins/common/snmp/translator_netsnmp.go @@ -225,6 +225,7 @@ func (n *netsnmpTranslator) snmpTranslateCall(oid string) (mibName string, oidNu oidText = oidText[i+2:] } + var b strings.Builder for scanner.Scan() { line := scanner.Text() @@ -247,18 +248,21 @@ func (n *netsnmpTranslator) snmpTranslateCall(oid string) (mibName string, oidNu if i := strings.Index(obj, "("); i != -1 { obj = obj[i+1:] if j := strings.Index(obj, ")"); j != -1 { - oidNum += "." + obj[:j] + b.WriteString(".") + b.WriteString(obj[:j]) } else { return "", "", "", "", fmt.Errorf("getting OID number from: %s", obj) } } else { - oidNum += "." + obj + b.WriteString(".") + b.WriteString(obj) } } break } } + oidNum = b.String() return mibName, oidNum, oidText, conversion, nil } diff --git a/plugins/inputs/gnmi/path.go b/plugins/inputs/gnmi/path.go index 39dcb80f753ed..5f11e5537b4e4 100644 --- a/plugins/inputs/gnmi/path.go +++ b/plugins/inputs/gnmi/path.go @@ -268,20 +268,25 @@ func (pi *pathInfo) relative(path *pathInfo, withNamespace bool) string { } segments := path.segments[len(pi.segments):len(path.segments)] - var r string + var b strings.Builder if withNamespace && segments[0].namespace != "" { - r = segments[0].namespace + ":" + segments[0].id + b.WriteString(segments[0].namespace) + b.WriteString(":") + b.WriteString(segments[0].id) } else { - r = segments[0].id + b.WriteString(segments[0].id) } for _, s := range segments[1:] { + b.WriteString("/") if withNamespace && s.namespace != "" { - r += "/" + s.namespace + ":" + s.id + b.WriteString(s.namespace) + b.WriteString(":") + b.WriteString(s.id) } else { - r += "/" + s.id + b.WriteString(s.id) } } - return r + return b.String() } func (pi *pathInfo) keepCommonPart(path *pathInfo) { @@ -310,18 +315,22 @@ func (pi *pathInfo) dir() string { return "" } - var dir string + var b strings.Builder if pi.origin != "" { - dir = pi.origin + ":" + b.WriteString(pi.origin) + b.WriteString(":") } for _, s := range pi.segments[:len(pi.segments)-1] { + b.WriteString("/") if s.namespace != "" { - dir += "/" + s.namespace + ":" + s.id + b.WriteString(s.namespace) + b.WriteString(":") + b.WriteString(s.id) } else { - dir += "/" + s.id + b.WriteString(s.id) } } - return dir + return b.String() } func (pi *pathInfo) base() string { @@ -341,31 +350,37 @@ func (pi *pathInfo) path() (origin, path string) { return pi.origin, "/" } + var b strings.Builder for _, s := range pi.segments { - path += "/" + s.id + b.WriteString("/") + b.WriteString(s.id) } - return pi.origin, path + return pi.origin, b.String() } func (pi *pathInfo) fullPath() string { - var path string + var b strings.Builder if pi.origin != "" { - path = pi.origin + ":" + b.WriteString(pi.origin) + b.WriteString(":") } if len(pi.segments) == 0 { - return path + return b.String() } for _, s := range pi.segments { + b.WriteString("/") if s.namespace != "" { - path += "/" + s.namespace + ":" + s.id + b.WriteString(s.namespace) + b.WriteString(":") + b.WriteString(s.id) } else { - path += "/" + s.id + b.WriteString(s.id) } } - return path + return b.String() } func (pi *pathInfo) String() string { @@ -374,10 +389,13 @@ func (pi *pathInfo) String() string { } origin, path := pi.path() + var b strings.Builder if origin != "" { - return origin + ":" + path + b.WriteString(origin) + b.WriteString(":") } - return path + b.WriteString(path) + return b.String() } func (pi *pathInfo) tags(pathPrefix bool) map[string]string { @@ -387,15 +405,26 @@ func (pi *pathInfo) tags(pathPrefix bool) map[string]string { if pathPrefix && s.name != "" { prefix = s.name + "_" } + // precompute constant path prefix for this keySegment + pathPrefixStr := s.path + "/" + for k, v := range s.kv { - key := strings.ReplaceAll(prefix+k, "-", "_") + // build the key (prefix + k) and sanitize in one builder + var kb strings.Builder + kb.WriteString(prefix) + kb.WriteString(k) + key := strings.ReplaceAll(kb.String(), "-", "_") // Use short-form of key if possible if _, exists := tags[key]; !exists { tags[key] = v continue } - tags[s.path+"/"+key] = v + // build full path/key only when needed + var fb strings.Builder + fb.WriteString(pathPrefixStr) + fb.WriteString(key) + tags[fb.String()] = v } } diff --git a/plugins/inputs/intel_rdt/intel_rdt.go b/plugins/inputs/intel_rdt/intel_rdt.go index d1427fbdb4f00..f6e1ac6b5f046 100644 --- a/plugins/inputs/intel_rdt/intel_rdt.go +++ b/plugins/inputs/intel_rdt/intel_rdt.go @@ -355,20 +355,22 @@ func shutDownPqos(pqos *exec.Cmd) error { func createArgCores(cores []string) string { allGroupsArg := "--mon-core=" + var b strings.Builder for _, coreGroup := range cores { argGroup := createArgsForGroups(strings.Split(coreGroup, ",")) - allGroupsArg = allGroupsArg + argGroup + b.WriteString(argGroup) } - return allGroupsArg + return allGroupsArg + b.String() } func createArgProcess(processPIDs map[string]string) string { allPIDsArg := "--mon-pid=" + var b strings.Builder for _, PIDs := range processPIDs { argPIDs := createArgsForGroups(strings.Split(PIDs, ",")) - allPIDsArg = allPIDsArg + argPIDs + b.WriteString(argPIDs) } - return allPIDsArg + return allPIDsArg + b.String() } func createArgsForGroups(coresOrPIDs []string) string { diff --git a/plugins/inputs/modbus/modbus.go b/plugins/inputs/modbus/modbus.go index eeb6577a8c5ae..fe30aa87bc29d 100644 --- a/plugins/inputs/modbus/modbus.go +++ b/plugins/inputs/modbus/modbus.go @@ -9,6 +9,7 @@ import ( "net/url" "path/filepath" "strconv" + "strings" "time" mb "github.com/grid-x/modbus" @@ -118,13 +119,15 @@ func (m *Modbus) SampleConfig() string { &m.configurationPerMetric, } - totalConfig := sampleConfigStart + var b strings.Builder + b.WriteString(sampleConfigStart) for _, c := range configs { - totalConfig += c.sampleConfigPart() + "\n" + b.WriteString(c.sampleConfigPart()) + b.WriteString("\n") } - totalConfig += "\n" - totalConfig += sampleConfigEnd - return totalConfig + b.WriteString("\n") + b.WriteString(sampleConfigEnd) + return b.String() } func (m *Modbus) Init() error { diff --git a/plugins/inputs/mysql/mysql.go b/plugins/inputs/mysql/mysql.go index 2ec1e6b4c2872..ffa62120e6c8c 100644 --- a/plugins/inputs/mysql/mysql.go +++ b/plugins/inputs/mysql/mysql.go @@ -1374,8 +1374,6 @@ func (m *Mysql) gatherInnoDBMetrics(db *sql.DB, servtag string, acc telegraf.Acc // gatherPerfSummaryPerAccountPerEvent can be used to fetch enabled metrics from // performance_schema.events_statements_summary_by_account_by_event_name func (m *Mysql) gatherPerfSummaryPerAccountPerEvent(db *sql.DB, servtag string, acc telegraf.Accumulator) error { - sqlQuery := perfSummaryPerAccountPerEvent - var rows *sql.Rows var err error @@ -1412,17 +1410,19 @@ func (m *Mysql) gatherPerfSummaryPerAccountPerEvent(db *sql.DB, servtag string, var events []interface{} // if we have perf_summary_events set - select only listed events (adding filter criteria for rows) if len(m.PerfSummaryEvents) > 0 { - sqlQuery += " WHERE EVENT_NAME IN (" + var sqlQueryBuilder strings.Builder + sqlQueryBuilder.WriteString(perfSummaryPerAccountPerEvent) + sqlQueryBuilder.WriteString(" WHERE EVENT_NAME IN (") for i, eventName := range m.PerfSummaryEvents { if i > 0 { - sqlQuery += ", " + sqlQueryBuilder.WriteString(", ") } - sqlQuery += "?" + sqlQueryBuilder.WriteString("?") events = append(events, eventName) } - sqlQuery += ")" + sqlQueryBuilder.WriteString(")") - rows, err = db.Query(sqlQuery, events...) + rows, err = db.Query(sqlQueryBuilder.String(), events...) } else { // otherwise no filter, hence, select all rows rows, err = db.Query(perfSummaryPerAccountPerEvent) diff --git a/plugins/inputs/opcua_listener/opcua_listener_test.go b/plugins/inputs/opcua_listener/opcua_listener_test.go index aa4359bdd5d3b..8587589d50b1a 100644 --- a/plugins/inputs/opcua_listener/opcua_listener_test.go +++ b/plugins/inputs/opcua_listener/opcua_listener_test.go @@ -3,6 +3,7 @@ package opcua_listener import ( "context" "fmt" + "strings" "testing" "time" @@ -230,12 +231,12 @@ func TestSubscribeClientIntegration(t *testing.T) { } case <-ctx.Done(): - msg := "" + var sb strings.Builder for _, tag := range tagsRemaining { - msg += tag + ", " + sb.WriteString(tag) + sb.WriteString(", ") } - - t.Errorf("Tags %s are remaining without a received value", msg) + t.Errorf("Tags %s are remaining without a received value", sb.String()) return } } @@ -371,12 +372,12 @@ func TestSubscribeClientIntegrationAdditionalFields(t *testing.T) { } case <-ctx.Done(): - msg := "" + var sb strings.Builder for _, tag := range tagsRemaining { - msg += tag + ", " + sb.WriteString(tag) + sb.WriteString(", ") } - - t.Errorf("Tags %s are remaining without a received value", msg) + t.Errorf("Tags %s are remaining without a received value", sb.String()) return } } diff --git a/plugins/inputs/passenger/passenger_test.go b/plugins/inputs/passenger/passenger_test.go index 49411d04919d5..e14e0068140e8 100644 --- a/plugins/inputs/passenger/passenger_test.go +++ b/plugins/inputs/passenger/passenger_test.go @@ -17,10 +17,14 @@ func fakePassengerStatus(stat string) (string, error) { var fileExtension, content string if runtime.GOOS == "windows" { fileExtension = ".bat" - content = "@echo off\n" + var sb strings.Builder + sb.WriteString("@echo off\n") for _, line := range strings.Split(strings.TrimSuffix(stat, "\n"), "\n") { - content += "for /f \"delims=\" %%A in (\"" + line + "\") do echo %%~A\n" // my eyes are bleeding + sb.WriteString("for /f \"delims=\" %%A in (\"") + sb.WriteString(line) + sb.WriteString("\") do echo %%~A\n") } + content = sb.String() } else { content = fmt.Sprintf("#!/bin/sh\ncat << EOF\n%s\nEOF", stat) } diff --git a/plugins/inputs/phpfpm/fcgi.go b/plugins/inputs/phpfpm/fcgi.go index 3fa8ef467539f..b09a23a02b52b 100644 --- a/plugins/inputs/phpfpm/fcgi.go +++ b/plugins/inputs/phpfpm/fcgi.go @@ -213,6 +213,7 @@ func encodeSize(b []byte, size uint32) int { binary.BigEndian.PutUint32(b, size) return 4 } + //nolint:gosec // G602: False positive — all callers allocate b with length >= 1 (e.g., make([]byte,4) or make([]byte,8)), so indexing b[0] is safe. b[0] = byte(size) return 1 } diff --git a/plugins/inputs/postgresql/postgresql_test.go b/plugins/inputs/postgresql/postgresql_test.go index 0a9f998ad1cae..003b2d75ec1d1 100644 --- a/plugins/inputs/postgresql/postgresql_test.go +++ b/plugins/inputs/postgresql/postgresql_test.go @@ -110,6 +110,7 @@ func TestPostgresqlGeneratesMetricsIntegration(t *testing.T) { metricsCounted++ } + //nolint:gosec // G602: False positive — this is a safe range iteration over a slice (it may be empty) for _, metric := range int32Metrics { require.True(t, acc.HasInt32Field("postgresql", metric), "%q not found in int32 metrics", metric) metricsCounted++ diff --git a/plugins/inputs/postgresql_extensible/postgresql_extensible_test.go b/plugins/inputs/postgresql_extensible/postgresql_extensible_test.go index 9e3db7a6796a1..813889ec27dfe 100644 --- a/plugins/inputs/postgresql_extensible/postgresql_extensible_test.go +++ b/plugins/inputs/postgresql_extensible/postgresql_extensible_test.go @@ -106,6 +106,7 @@ func TestPostgresqlGeneratesMetricsIntegration(t *testing.T) { metricsCounted++ } + //nolint:gosec // G602: False positive — this is a safe range iteration over a slice (it may be empty) for _, metric := range int32Metrics { require.True(t, acc.HasInt32Field("postgresql", metric)) metricsCounted++ @@ -221,6 +222,7 @@ func TestPostgresqlFieldOutputIntegration(t *testing.T) { require.Truef(t, found, "expected %s to be an integer", field) } + //nolint:gosec // G602: False positive — this is a safe range iteration over a slice (it may be empty) for _, field := range int32Metrics { _, found := acc.Int32Field(measurement, field) require.Truef(t, found, "expected %s to be an int32", field) diff --git a/plugins/inputs/ravendb/ravendb.go b/plugins/inputs/ravendb/ravendb.go index 235e76fadd157..8a45665974682 100644 --- a/plugins/inputs/ravendb/ravendb.go +++ b/plugins/inputs/ravendb/ravendb.go @@ -376,12 +376,15 @@ func prepareDBNamesURLPart(dbNames []string) string { if len(dbNames) == 0 { return "" } - result := "?" + dbNames[0] + var b strings.Builder + b.WriteString("?") + b.WriteString(dbNames[0]) for _, db := range dbNames[1:] { - result += "&name=" + url.QueryEscape(db) + b.WriteString("&name=") + b.WriteString(url.QueryEscape(db)) } - return result + return b.String() } func init() { diff --git a/plugins/outputs/loki/loki.go b/plugins/outputs/loki/loki.go index 02b4983e551cc..ef1bdc2d2527d 100644 --- a/plugins/outputs/loki/loki.go +++ b/plugins/outputs/loki/loki.go @@ -135,10 +135,11 @@ func (l *Loki) Write(metrics []telegraf.Metric) error { } } - var line string + var lineBuilder strings.Builder for _, f := range m.FieldList() { - line += fmt.Sprintf("%s=\"%v\" ", f.Key, f.Value) + lineBuilder.WriteString(fmt.Sprintf("%s=\"%v\" ", f.Key, f.Value)) } + line := lineBuilder.String() s.insertLog(tags, Log{strconv.FormatInt(m.Time().UnixNano(), 10), line}) } diff --git a/plugins/outputs/loki/stream.go b/plugins/outputs/loki/stream.go index 5f1687ef2fb3b..c26618c4812fd 100644 --- a/plugins/outputs/loki/stream.go +++ b/plugins/outputs/loki/stream.go @@ -46,14 +46,14 @@ func (s Streams) MarshalJSON() ([]byte, error) { } func uniqKeyFromTagList(ts []*telegraf.Tag) (k string) { + var b strings.Builder for _, t := range ts { - k += fmt.Sprintf("%s-%s-", + b.WriteString(fmt.Sprintf("%s-%s-", strings.ReplaceAll(t.Key, "-", "--"), strings.ReplaceAll(t.Value, "-", "--"), - ) + )) } - - return k + return b.String() } func newStream(ts []*telegraf.Tag) *Stream { diff --git a/plugins/outputs/newrelic/newrelic.go b/plugins/outputs/newrelic/newrelic.go index bceca05daa6b4..681784f5076ce 100644 --- a/plugins/outputs/newrelic/newrelic.go +++ b/plugins/outputs/newrelic/newrelic.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "time" "github.com/newrelic/newrelic-telemetry-sdk-go/cumulative" @@ -58,12 +59,12 @@ func (nr *NewRelic) Connect() error { cfg.HarvestTimeout = time.Duration(nr.Timeout) cfg.Client = &nr.client cfg.ErrorLogger = func(e map[string]interface{}) { - var errorString string + var b strings.Builder for k, v := range e { - errorString += fmt.Sprintf("%s = %s ", k, v) + b.WriteString(fmt.Sprintf("%s = %s ", k, v)) } nr.errorCount++ - nr.savedErrors[nr.errorCount] = errorString + nr.savedErrors[nr.errorCount] = b.String() } if nr.MetricURL != "" { cfg.MetricsURLOverride = nr.MetricURL diff --git a/plugins/parsers/csv/parser_test.go b/plugins/parsers/csv/parser_test.go index 7b5e8d8fd55b8..632a9cb322384 100644 --- a/plugins/parsers/csv/parser_test.go +++ b/plugins/parsers/csv/parser_test.go @@ -1019,7 +1019,9 @@ timestamp,type,name,status rowIndex++ m, err = p.ParseLine(testCSVRows[rowIndex]) require.NoError(t, err) + //nolint:gosec // G602: False positive — length is known to be == 2 require.Equal(t, expectedFields[0], m.Fields()) + //nolint:gosec // G602: False positive — length is known to be == 2 require.Equal(t, expectedTags[0], m.Tags()) rowIndex++ m, err = p.ParseLine(testCSVRows[rowIndex]) @@ -1028,7 +1030,9 @@ timestamp,type,name,status rowIndex++ m, err = p.ParseLine(testCSVRows[rowIndex]) require.NoError(t, err) + //nolint:gosec // G602: False positive — length is known to be == 2 require.Equal(t, expectedFields[1], m.Fields()) + //nolint:gosec // G602: False positive — length is known to be == 2 require.Equal(t, expectedTags[1], m.Tags()) } diff --git a/plugins/processors/topk/topk.go b/plugins/processors/topk/topk.go index 41bcc9d584ff6..4276662202c93 100644 --- a/plugins/processors/topk/topk.go +++ b/plugins/processors/topk/topk.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "sort" + "strings" "time" "github.com/influxdata/telegraf" @@ -125,25 +126,33 @@ func (t *TopK) generateGroupByKey(m telegraf.Metric) (string, error) { } } - groupkey := m.Name() + "&" + var b strings.Builder + b.WriteString(m.Name()) + b.WriteString("&") if len(t.GroupBy) > 0 { tags := m.Tags() keys := make([]string, 0, len(tags)) + var kb strings.Builder for tag, value := range tags { if t.tagsGlobs.Match(tag) { - keys = append(keys, tag+"="+value+"&") + kb.Reset() + kb.WriteString(tag) + kb.WriteString("=") + kb.WriteString(value) + kb.WriteString("&") + keys = append(keys, kb.String()) } } // Sorting the selected tags is necessary because dictionaries // do not ensure any specific or deterministic ordering sort.SliceStable(keys, func(i, j int) bool { return keys[i] < keys[j] }) for _, str := range keys { - groupkey += str + b.WriteString(str) } } - return groupkey, nil + return b.String(), nil } func (t *TopK) groupBy(m telegraf.Metric) { diff --git a/plugins/serializers/graphite/graphite.go b/plugins/serializers/graphite/graphite.go index 269c631021df4..761bf8ea72486 100644 --- a/plugins/serializers/graphite/graphite.go +++ b/plugins/serializers/graphite/graphite.go @@ -325,16 +325,15 @@ func buildTags(tags map[string]string) string { } sort.Strings(keys) - var tagStr string + var b strings.Builder for i, k := range keys { tagValue := strings.ReplaceAll(tags[k], ".", "_") - if i == 0 { - tagStr += tagValue - } else { - tagStr += "." + tagValue + if i > 0 { + b.WriteByte('.') } + b.WriteString(tagValue) } - return tagStr + return b.String() } func (s *Serializer) strictSanitize(value string) string { diff --git a/plugins/serializers/prometheusremotewrite/prometheusremotewrite_test.go b/plugins/serializers/prometheusremotewrite/prometheusremotewrite_test.go index 5c282abbca689..45925dd9d2a7b 100644 --- a/plugins/serializers/prometheusremotewrite/prometheusremotewrite_test.go +++ b/plugins/serializers/prometheusremotewrite/prometheusremotewrite_test.go @@ -879,20 +879,22 @@ func prompbToHistogramText(data []byte) ([]byte, error) { // There is no text representation for native histogram and it has to be written out as proto exposition. // For test purpose we format a reasonable string for verification. Labels are sorted to make it deterministic. nameString := "" - labelString := "{" + var lb strings.Builder + lb.WriteByte('{') firstLabel := true for _, l := range ts.Labels { if l.Name == model.MetricNameLabel { nameString = l.Value } else { if !firstLabel { - labelString += ", " + lb.WriteString(", ") } - labelString += fmt.Sprintf("%s=%q", l.Name, l.Value) + fmt.Fprintf(&lb, "%s=%q", l.Name, l.Value) firstLabel = false } } - labelString += "}" + lb.WriteByte('}') + labelString := lb.String() for _, h := range ts.Histograms { fh := *h.ToFloatHistogram() buf.WriteString(nameString)