Compare commits

...

20 Commits

Author SHA1 Message Date
Eugene cc7f902c5c
Merge c52d495061 into a934c35951 2024-11-12 01:57:15 +02:00
Ivan Goncharov a934c35951
Add: exponential backoff for CAS operations on floats (#1661)
* add: exponential backoff for CAS operations of floats

Signed-off-by: Ivan Goncharov <i.morph@gmail.com>

* add: some more benchmark use cases (higher contention)

Signed-off-by: Ivan Goncharov <i.morph@gmail.com>

* fmt: fumpted some files

Signed-off-by: Ivan Goncharov <i.morph@gmail.com>

* add: license header

Signed-off-by: Ivan Goncharov <i.morph@gmail.com>

* add: comment explaining origin of backoff constants

Signed-off-by: Ivan Goncharov <i.morph@gmail.com>

---------

Signed-off-by: Ivan Goncharov <i.morph@gmail.com>
2024-11-11 16:26:17 +00:00
Matthieu MOREL bab92a7743
chore: enable usestdlibvars linter (#1680)
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
2024-11-11 15:19:33 +01:00
PrometheusBot 400ee29a10
Update common Prometheus files (#1679)
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2024-11-11 15:09:29 +01:00
Arthur Silva Sens 2b11a4ba39
Merge pull request #1673 from imorph/faster_find_bucket
PERF: faster algorithm to discover bucket of an histogram observation
2024-11-08 07:22:56 -03:00
Matthieu MOREL fcfad5c0b9
[chore]: enable perfsprint linter (#1676) 2024-11-08 09:54:31 +01:00
github-actions[bot] 1aa11d0498
Merge pull request #1678 from prometheus/dependabot/github_actions/github-actions-99b3cd78cd
build(deps): bump the github-actions group across 1 directory with 3 updates
2024-11-08 08:49:44 +00:00
dependabot[bot] abfb25769f
build(deps): bump the github-actions group across 1 directory with 3 updates
Bumps the github-actions group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [github/codeql-action](https://github.com/github/codeql-action) and [dagger/dagger-for-github](https://github.com/dagger/dagger-for-github).


Updates `actions/checkout` from 4.2.0 to 4.2.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](d632683dd7...11bd71901b)

Updates `github/codeql-action` from 3.26.10 to 3.27.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e2b3eafc8d...662472033e)

Updates `dagger/dagger-for-github` from 6.11.0 to 6.14.0
- [Release notes](https://github.com/dagger/dagger-for-github/releases)
- [Commits](fc945fa66f...ad6a4e308a)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: dagger/dagger-for-github
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-08 08:42:37 +00:00
Ben Kochie 6de54aaefd
Merge pull request #1674 from prometheus/repo_sync
Synchronize common files from prometheus/prometheus
2024-11-08 08:41:25 +00:00
Bartlomiej Plotka 02883cbc82
Merge pull request #1675 from mharbison72/issue-1660
process_collector: avoid a compiler warning on macOS (fixes #1660)
2024-11-08 00:52:32 +01:00
Matt Harbison 3c21cc0ecf process_collector: avoid a compiler warning on macOS (fixes #1660)
The header has a warning when included, with no way to shut it off, and no
alternative to obtain these symbols.  They're technically architecture specific
values, but they aren't different between amd64 and arm64, so combine the
definitions.

Signed-off-by: Matt Harbison <mharbison72@gmail.com>
2024-11-07 17:22:22 -05:00
prombot 8aea698d6a Update common Prometheus files
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2024-11-05 17:47:44 +00:00
Ivan Goncharov 78d7a94e46 add: linear search implementation (+ benchmarks)
Signed-off-by: Ivan Goncharov <i.morph@gmail.com>
2024-11-04 20:26:59 +01:00
Eugene c52d495061
promsafe: improvements. ability to add fast implementation of toLabelNames
Signed-off-by: Eugene <eugene@amberpixels.io>
2024-09-20 11:41:11 +03:00
Eugene 5b52b012e1
promauto: make it available to avoid reflect when building labels
Signed-off-by: Eugene <eugene@amberpixels.io>
2024-09-20 11:14:19 +03:00
Eugene 83a2a655ae
add example showing that using pointer to labels struct is fine
Signed-off-by: Eugene <eugene@amberpixels.io>
2024-09-20 00:32:13 +03:00
Eugene 6724abaa0b
simplify SingleLabeled metrics: just accept a string when registering a metric
Signed-off-by: Eugene <eugene@amberpixels.io>
2024-09-20 00:19:09 +03:00
Eugene 80149ab4d4
clearer & simpler API. move out single_label_provider as a separate case
Signed-off-by: Eugene <eugene@amberpixels.io>
2024-09-02 13:39:26 +03:00
Eugene aa7e203147
cleaner example
Signed-off-by: Eugene <eugene@amberpixels.io>
2024-08-31 11:06:19 +03:00
Eugene 5d421be191
Stongly typed labels: `promsafe` feature introduced
Signed-off-by: Eugene <eugene@amberpixels.io>
2024-08-28 19:13:51 +03:00
31 changed files with 1042 additions and 133 deletions

View File

@ -46,11 +46,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -61,7 +61,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -75,4 +75,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0

View File

@ -18,7 +18,7 @@ jobs:
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
steps:
- name: git checkout
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set docker hub repo name
run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
- name: Push README to Dockerhub
@ -40,7 +40,7 @@ jobs:
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
steps:
- name: git checkout
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set quay.io org name
run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV
- name: Set quay.io repo name

View File

@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Lint
uses: dagger/dagger-for-github@fc945fa66fc7bfa72bc80f85b1a1ef4bd1d30cbb # v6.11.0
uses: dagger/dagger-for-github@ad6a4e308a42fb2cf9be8b060f9aba9d57d4c9aa # v6.14.0
with:
version: "latest"
verb: call

View File

@ -24,7 +24,7 @@ jobs:
supported_versions: ${{ steps.matrix.outputs.supported_versions }}
steps:
- name: Checkout code
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Read supported_go_versions.txt
id: matrix
run: |
@ -43,17 +43,17 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run tests and check license
uses: dagger/dagger-for-github@fc945fa66fc7bfa72bc80f85b1a1ef4bd1d30cbb # v6.11.0
uses: dagger/dagger-for-github@ad6a4e308a42fb2cf9be8b060f9aba9d57d4c9aa # v6.14.0
with:
version: "latest"
verb: call
args: -vvv --src . make --go-version ${{matrix.go_version}} --args 'check_license test'
- name: Run style and unused
uses: dagger/dagger-for-github@fc945fa66fc7bfa72bc80f85b1a1ef4bd1d30cbb # v6.11.0
uses: dagger/dagger-for-github@ad6a4e308a42fb2cf9be8b060f9aba9d57d4c9aa # v6.14.0
if: ${{ matrix.go_version == '1.21' }}
with:
version: "latest"

View File

@ -24,16 +24,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: 1.23.x
- name: Install snmp_exporter/generator dependencies
run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
if: github.repository == 'prometheus/snmp_exporter'
- name: Lint
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with:
args: --verbose
version: v1.60.2
version: v1.61.0

View File

@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Execute bash script
run: bash update-go-version.bash

View File

@ -25,11 +25,13 @@ linters:
- ineffassign
- misspell
- nolintlint
- perfsprint
- predeclared
- revive
- staticcheck
- unconvert
- unused
- usestdlibvars
- wastedassign
issues:
@ -66,6 +68,17 @@ linters-settings:
local-prefixes: github.com/prometheus/client_golang
gofumpt:
extra-rules: true
perfsprint:
# Optimizes even if it requires an int or uint type cast.
int-conversion: true
# Optimizes into `err.Error()` even if it is only equivalent for non-nil errors.
err-error: true
# Optimizes `fmt.Errorf`.
errorf: true
# Optimizes `fmt.Sprintf` with only one argument.
sprintf1: true
# Optimizes into strings concatenation.
strconcat: true
revive:
rules:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter

View File

@ -16,13 +16,13 @@ package v1
import (
"context"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"time"
@ -260,7 +260,7 @@ func TestAPIs(t *testing.T) {
},
{
do: doQuery("2", testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "POST",
reqPath: "/api/v1/query",
@ -336,7 +336,7 @@ func TestAPIs(t *testing.T) {
End: testTime,
Step: 1 * time.Minute,
}, WithTimeout(5*time.Second)),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "POST",
reqPath: "/api/v1/query_range",
@ -361,14 +361,14 @@ func TestAPIs(t *testing.T) {
{
do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "POST",
reqPath: "/api/v1/labels",
err: errors.New("some error"),
},
{
do: doLabelNames(nil, testTime.Add(-100*time.Hour), testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
inWarnings: []string{"a"},
reqMethod: "POST",
reqPath: "/api/v1/labels",
@ -400,14 +400,14 @@ func TestAPIs(t *testing.T) {
{
do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "GET",
reqPath: "/api/v1/label/mylabel/values",
err: errors.New("some error"),
},
{
do: doLabelValues(nil, "mylabel", testTime.Add(-100*time.Hour), testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
inWarnings: []string{"a"},
reqMethod: "GET",
reqPath: "/api/v1/label/mylabel/values",
@ -464,7 +464,7 @@ func TestAPIs(t *testing.T) {
{
do: doSeries("up", testTime.Add(-time.Minute), testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "POST",
reqPath: "/api/v1/series",
err: errors.New("some error"),
@ -472,7 +472,7 @@ func TestAPIs(t *testing.T) {
// Series with error and warning.
{
do: doSeries("up", testTime.Add(-time.Minute), testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
inWarnings: []string{"a"},
reqMethod: "POST",
reqPath: "/api/v1/series",
@ -493,7 +493,7 @@ func TestAPIs(t *testing.T) {
{
do: doSnapshot(true),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "POST",
reqPath: "/api/v1/admin/tsdb/snapshot",
err: errors.New("some error"),
@ -507,7 +507,7 @@ func TestAPIs(t *testing.T) {
{
do: doCleanTombstones(),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "POST",
reqPath: "/api/v1/admin/tsdb/clean_tombstones",
err: errors.New("some error"),
@ -528,7 +528,7 @@ func TestAPIs(t *testing.T) {
{
do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "POST",
reqPath: "/api/v1/admin/tsdb/delete_series",
err: errors.New("some error"),
@ -550,8 +550,8 @@ func TestAPIs(t *testing.T) {
do: doConfig(),
reqMethod: "GET",
reqPath: "/api/v1/status/config",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -578,16 +578,16 @@ func TestAPIs(t *testing.T) {
do: doFlags(),
reqMethod: "GET",
reqPath: "/api/v1/status/flags",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
do: doBuildinfo(),
reqMethod: "GET",
reqPath: "/api/v1/status/buildinfo",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -616,8 +616,8 @@ func TestAPIs(t *testing.T) {
do: doRuntimeinfo(),
reqMethod: "GET",
reqPath: "/api/v1/status/runtimeinfo",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -684,8 +684,8 @@ func TestAPIs(t *testing.T) {
do: doAlertManagers(),
reqMethod: "GET",
reqPath: "/api/v1/alertmanagers",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -891,8 +891,8 @@ func TestAPIs(t *testing.T) {
do: doRules(),
reqMethod: "GET",
reqPath: "/api/v1/rules",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -971,8 +971,8 @@ func TestAPIs(t *testing.T) {
do: doTargets(),
reqMethod: "GET",
reqPath: "/api/v1/targets",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -1005,7 +1005,7 @@ func TestAPIs(t *testing.T) {
{
do: doTargetsMetadata("{job=\"prometheus\"}", "go_goroutines", "1"),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "GET",
reqPath: "/api/v1/targets/metadata",
err: errors.New("some error"),
@ -1037,7 +1037,7 @@ func TestAPIs(t *testing.T) {
{
do: doMetadata("", "1"),
inErr: fmt.Errorf("some error"),
inErr: errors.New("some error"),
reqMethod: "GET",
reqPath: "/api/v1/metadata",
err: errors.New("some error"),
@ -1047,8 +1047,8 @@ func TestAPIs(t *testing.T) {
do: doTSDB(),
reqMethod: "GET",
reqPath: "/api/v1/status/tsdb",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -1127,8 +1127,8 @@ func TestAPIs(t *testing.T) {
do: doWalReply(),
reqMethod: "GET",
reqPath: "/api/v1/status/walreplay",
inErr: fmt.Errorf("some error"),
err: fmt.Errorf("some error"),
inErr: errors.New("some error"),
err: errors.New("some error"),
},
{
@ -1212,7 +1212,7 @@ func TestAPIs(t *testing.T) {
tests = append(tests, queryTests...)
for i, test := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
t.Run(strconv.Itoa(i), func(t *testing.T) {
tc.curTest = test
res, warnings, err := test.do()
@ -1430,7 +1430,7 @@ func TestAPIClientDo(t *testing.T) {
}
for i, test := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
t.Run(strconv.Itoa(i), func(t *testing.T) {
tc.ch <- test
_, body, warnings, err := client.Do(context.Background(), tc.req)

View File

@ -17,10 +17,10 @@
package main
import (
"fmt"
"log"
"math/rand"
"net/http"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
@ -50,7 +50,7 @@ func main() {
// Record fictional latency.
now := time.Now()
requestDurations.(prometheus.ExemplarObserver).ObserveWithExemplar(
time.Since(now).Seconds(), prometheus.Labels{"dummyID": fmt.Sprint(rand.Intn(100000))},
time.Since(now).Seconds(), prometheus.Labels{"dummyID": strconv.Itoa(rand.Intn(100000))},
)
time.Sleep(600 * time.Millisecond)
}

View File

@ -18,11 +18,11 @@ package main
import (
"flag"
"fmt"
"log"
"math"
"math/rand"
"net/http"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
@ -116,7 +116,7 @@ func main() {
// the ExemplarObserver interface and thus don't need to
// check the outcome of the type assertion.
m.rpcDurationsHistogram.(prometheus.ExemplarObserver).ObserveWithExemplar(
v, prometheus.Labels{"dummyID": fmt.Sprint(rand.Intn(100000))},
v, prometheus.Labels{"dummyID": strconv.Itoa(rand.Intn(100000))},
)
time.Sleep(time.Duration(75*oscillationFactor()) * time.Millisecond)
}

View File

@ -0,0 +1,50 @@
// Copyright 2014 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"math"
"sync/atomic"
"time"
)
// atomicUpdateFloat atomically updates the float64 value pointed to by bits
// using the provided updateFunc, with an exponential backoff on contention.
func atomicUpdateFloat(bits *uint64, updateFunc func(float64) float64) {
const (
// both numbers are derived from empirical observations
// documented in this PR: https://github.com/prometheus/client_golang/pull/1661
maxBackoff = 320 * time.Millisecond
initialBackoff = 10 * time.Millisecond
)
backoff := initialBackoff
for {
loadedBits := atomic.LoadUint64(bits)
oldFloat := math.Float64frombits(loadedBits)
newFloat := updateFunc(oldFloat)
newBits := math.Float64bits(newFloat)
if atomic.CompareAndSwapUint64(bits, loadedBits, newBits) {
break
} else {
// Exponential backoff with sleep and cap to avoid infinite wait
time.Sleep(backoff)
backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
}

View File

@ -0,0 +1,167 @@
// Copyright 2014 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"math"
"sync"
"sync/atomic"
"testing"
"unsafe"
)
var output float64
func TestAtomicUpdateFloat(t *testing.T) {
var val float64 = 0.0
bits := (*uint64)(unsafe.Pointer(&val))
var wg sync.WaitGroup
numGoroutines := 100000
increment := 1.0
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomicUpdateFloat(bits, func(f float64) float64 {
return f + increment
})
}()
}
wg.Wait()
expected := float64(numGoroutines) * increment
if val != expected {
t.Errorf("Expected %f, got %f", expected, val)
}
}
// Benchmark for atomicUpdateFloat with single goroutine (no contention).
func BenchmarkAtomicUpdateFloat_SingleGoroutine(b *testing.B) {
var val float64 = 0.0
bits := (*uint64)(unsafe.Pointer(&val))
for i := 0; i < b.N; i++ {
atomicUpdateFloat(bits, func(f float64) float64 {
return f + 1.0
})
}
output = val
}
// Benchmark for old implementation with single goroutine (no contention) -> to check overhead of backoff
func BenchmarkAtomicNoBackoff_SingleGoroutine(b *testing.B) {
var val float64 = 0.0
bits := (*uint64)(unsafe.Pointer(&val))
for i := 0; i < b.N; i++ {
for {
loadedBits := atomic.LoadUint64(bits)
newBits := math.Float64bits(math.Float64frombits(loadedBits) + 1.0)
if atomic.CompareAndSwapUint64(bits, loadedBits, newBits) {
break
}
}
}
output = val
}
// Benchmark varying the number of goroutines.
func benchmarkAtomicUpdateFloatConcurrency(b *testing.B, numGoroutines int) {
var val float64 = 0.0
bits := (*uint64)(unsafe.Pointer(&val))
b.SetParallelism(numGoroutines)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
atomicUpdateFloat(bits, func(f float64) float64 {
return f + 1.0
})
}
})
output = val
}
func benchmarkAtomicNoBackoffFloatConcurrency(b *testing.B, numGoroutines int) {
var val float64 = 0.0
bits := (*uint64)(unsafe.Pointer(&val))
b.SetParallelism(numGoroutines)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for {
loadedBits := atomic.LoadUint64(bits)
newBits := math.Float64bits(math.Float64frombits(loadedBits) + 1.0)
if atomic.CompareAndSwapUint64(bits, loadedBits, newBits) {
break
}
}
}
})
output = val
}
func BenchmarkAtomicUpdateFloat_1Goroutine(b *testing.B) {
benchmarkAtomicUpdateFloatConcurrency(b, 1)
}
func BenchmarkAtomicNoBackoff_1Goroutine(b *testing.B) {
benchmarkAtomicNoBackoffFloatConcurrency(b, 1)
}
func BenchmarkAtomicUpdateFloat_2Goroutines(b *testing.B) {
benchmarkAtomicUpdateFloatConcurrency(b, 2)
}
func BenchmarkAtomicNoBackoff_2Goroutines(b *testing.B) {
benchmarkAtomicNoBackoffFloatConcurrency(b, 2)
}
func BenchmarkAtomicUpdateFloat_4Goroutines(b *testing.B) {
benchmarkAtomicUpdateFloatConcurrency(b, 4)
}
func BenchmarkAtomicNoBackoff_4Goroutines(b *testing.B) {
benchmarkAtomicNoBackoffFloatConcurrency(b, 4)
}
func BenchmarkAtomicUpdateFloat_8Goroutines(b *testing.B) {
benchmarkAtomicUpdateFloatConcurrency(b, 8)
}
func BenchmarkAtomicNoBackoff_8Goroutines(b *testing.B) {
benchmarkAtomicNoBackoffFloatConcurrency(b, 8)
}
func BenchmarkAtomicUpdateFloat_16Goroutines(b *testing.B) {
benchmarkAtomicUpdateFloatConcurrency(b, 16)
}
func BenchmarkAtomicNoBackoff_16Goroutines(b *testing.B) {
benchmarkAtomicNoBackoffFloatConcurrency(b, 16)
}
func BenchmarkAtomicUpdateFloat_32Goroutines(b *testing.B) {
benchmarkAtomicUpdateFloatConcurrency(b, 32)
}
func BenchmarkAtomicNoBackoff_32Goroutines(b *testing.B) {
benchmarkAtomicNoBackoffFloatConcurrency(b, 32)
}

View File

@ -134,13 +134,9 @@ func (c *counter) Add(v float64) {
return
}
for {
oldBits := atomic.LoadUint64(&c.valBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
if atomic.CompareAndSwapUint64(&c.valBits, oldBits, newBits) {
return
}
}
atomicUpdateFloat(&c.valBits, func(oldVal float64) float64 {
return oldVal + v
})
}
func (c *counter) AddWithExemplar(v float64, e Labels) {

View File

@ -14,7 +14,6 @@
package prometheus
import (
"fmt"
"math"
"strings"
"testing"
@ -120,10 +119,10 @@ func TestCounterVecGetMetricWithInvalidLabelValues(t *testing.T) {
expectPanic(t, func() {
counterVec.WithLabelValues(labelValues...)
}, fmt.Sprintf("WithLabelValues: expected panic because: %s", test.desc))
}, "WithLabelValues: expected panic because: "+test.desc)
expectPanic(t, func() {
counterVec.With(test.labels)
}, fmt.Sprintf("WithLabelValues: expected panic because: %s", test.desc))
}, "WithLabelValues: expected panic because: "+test.desc)
if _, err := counterVec.GetMetricWithLabelValues(labelValues...); err == nil {
t.Errorf("GetMetricWithLabelValues: expected error because: %s", test.desc)

View File

@ -120,13 +120,9 @@ func (g *gauge) Dec() {
}
func (g *gauge) Add(val float64) {
for {
oldBits := atomic.LoadUint64(&g.valBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + val)
if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) {
return
}
}
atomicUpdateFloat(&g.valBits, func(oldVal float64) float64 {
return oldVal + val
})
}
func (g *gauge) Sub(val float64) {

View File

@ -858,15 +858,35 @@ func (h *histogram) Write(out *dto.Metric) error {
// findBucket returns the index of the bucket for the provided value, or
// len(h.upperBounds) for the +Inf bucket.
func (h *histogram) findBucket(v float64) int {
// TODO(beorn7): For small numbers of buckets (<30), a linear search is
// slightly faster than the binary search. If we really care, we could
// switch from one search strategy to the other depending on the number
// of buckets.
//
// Microbenchmarks (BenchmarkHistogramNoLabels):
// 11 buckets: 38.3 ns/op linear - binary 48.7 ns/op
// 100 buckets: 78.1 ns/op linear - binary 54.9 ns/op
// 300 buckets: 154 ns/op linear - binary 61.6 ns/op
n := len(h.upperBounds)
if n == 0 {
return 0
}
// Early exit: if v is less than or equal to the first upper bound, return 0
if v <= h.upperBounds[0] {
return 0
}
// Early exit: if v is greater than the last upper bound, return len(h.upperBounds)
if v > h.upperBounds[n-1] {
return n
}
// For small arrays, use simple linear search
// "magic number" 35 is result of tests on couple different (AWS and baremetal) servers
// see more details here: https://github.com/prometheus/client_golang/pull/1662
if n < 35 {
for i, bound := range h.upperBounds {
if v <= bound {
return i
}
}
// If v is greater than all upper bounds, return len(h.upperBounds)
return n
}
// For larger arrays, use stdlib's binary search
return sort.SearchFloat64s(h.upperBounds, v)
}
@ -1621,13 +1641,9 @@ func waitForCooldown(count uint64, counts *histogramCounts) {
// atomicAddFloat adds the provided float atomically to another float
// represented by the bit pattern the bits pointer is pointing to.
func atomicAddFloat(bits *uint64, v float64) {
for {
loadedBits := atomic.LoadUint64(bits)
newBits := math.Float64bits(math.Float64frombits(loadedBits) + v)
if atomic.CompareAndSwapUint64(bits, loadedBits, newBits) {
break
}
}
atomicUpdateFloat(bits, func(oldVal float64) float64 {
return oldVal + v
})
}
// atomicDecUint32 atomically decrements the uint32 p points to. See

View File

@ -1455,3 +1455,91 @@ func compareNativeExemplarValues(t *testing.T, exps []*dto.Exemplar, values []fl
}
}
}
var resultFindBucket int
func benchmarkFindBucket(b *testing.B, l int) {
h := &histogram{upperBounds: make([]float64, l)}
for i := range h.upperBounds {
h.upperBounds[i] = float64(i)
}
v := float64(l / 2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
resultFindBucket = h.findBucket(v)
}
}
func BenchmarkFindBucketShort(b *testing.B) {
benchmarkFindBucket(b, 20)
}
func BenchmarkFindBucketMid(b *testing.B) {
benchmarkFindBucket(b, 40)
}
func BenchmarkFindBucketLarge(b *testing.B) {
benchmarkFindBucket(b, 100)
}
func BenchmarkFindBucketHuge(b *testing.B) {
benchmarkFindBucket(b, 500)
}
func BenchmarkFindBucketInf(b *testing.B) {
h := &histogram{upperBounds: make([]float64, 500)}
for i := range h.upperBounds {
h.upperBounds[i] = float64(i)
}
v := 1000.5
b.ResetTimer()
for i := 0; i < b.N; i++ {
resultFindBucket = h.findBucket(v)
}
}
func BenchmarkFindBucketLow(b *testing.B) {
h := &histogram{upperBounds: make([]float64, 500)}
for i := range h.upperBounds {
h.upperBounds[i] = float64(i)
}
v := -1.1
b.ResetTimer()
for i := 0; i < b.N; i++ {
resultFindBucket = h.findBucket(v)
}
}
func TestFindBucket(t *testing.T) {
smallHistogram := &histogram{upperBounds: []float64{1, 2, 3, 4, 5}}
largeHistogram := &histogram{upperBounds: make([]float64, 50)}
for i := range largeHistogram.upperBounds {
largeHistogram.upperBounds[i] = float64(i)
}
tests := []struct {
h *histogram
v float64
expected int
}{
{smallHistogram, -1, 0},
{smallHistogram, 0.5, 0},
{smallHistogram, 2.5, 2},
{smallHistogram, 5.5, 5},
{largeHistogram, -1, 0},
{largeHistogram, 25.5, 26},
{largeHistogram, 49.5, 50},
{largeHistogram, 50.5, 50},
{largeHistogram, 5000.5, 50},
}
for _, tt := range tests {
result := tt.h.findBucket(tt.v)
if result != tt.expected {
t.Errorf("findBucket(%v) = %d; expected %d", tt.v, result, tt.expected)
}
}
}

View File

@ -22,6 +22,7 @@ import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
@ -524,7 +525,7 @@ func formatRangeUnified(start, stop int) string {
beginning := start + 1 // lines start numbering with one
length := stop - start
if length == 1 {
return fmt.Sprintf("%d", beginning)
return strconv.Itoa(beginning)
}
if length == 0 {
beginning-- // empty ranges begin at line just before the range

View File

@ -15,12 +15,16 @@
#include <mach/mach_init.h>
#include <mach/task.h>
// Compiler warns that shared_memory_server.h is deprecated, use this instead.
// But this doesn't define SHARED_DATA_REGION_SIZE or SHARED_TEXT_REGION_SIZE.
//#include <mach/shared_region.h>
#include <mach/shared_memory_server.h>
#include <mach/mach_vm.h>
// The compiler warns that mach/shared_memory_server.h is deprecated, and to use
// mach/shared_region.h instead. But that doesn't define
// SHARED_DATA_REGION_SIZE or SHARED_TEXT_REGION_SIZE, so redefine them here and
// avoid a warning message when running tests.
#define GLOBAL_SHARED_TEXT_SEGMENT 0x90000000U
#define SHARED_DATA_REGION_SIZE 0x10000000
#define SHARED_TEXT_REGION_SIZE 0x10000000
int get_memory_info(unsigned long long *rss, unsigned long long *vsize)
{

View File

@ -22,9 +22,7 @@ import "C"
import "fmt"
func getMemory() (*memoryInfo, error) {
var (
rss, vsize C.ulonglong
)
var rss, vsize C.ulonglong
if err := C.get_memory_info(&rss, &vsize); err != 0 {
return nil, fmt.Errorf("task_info() failed with 0x%x", int(err))

View File

@ -98,7 +98,7 @@ func readCompressedBody(r io.Reader, comp Compression) (string, error) {
got, err := io.ReadAll(reader)
return string(got), err
}
return "", fmt.Errorf("Unsupported compression")
return "", errors.New("Unsupported compression")
}
func TestHandlerErrorHandling(t *testing.T) {
@ -131,7 +131,7 @@ func TestHandlerErrorHandling(t *testing.T) {
logger := log.New(logBuf, "", 0)
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
request.Header.Add("Accept", "test/plain")
mReg := &mockTransactionGatherer{g: reg}
@ -252,7 +252,7 @@ func TestInstrumentMetricHandler(t *testing.T) {
// Do it again to test idempotency.
InstrumentMetricHandler(reg, HandlerForTransactional(mReg, HandlerOpts{}))
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
request.Header.Add(acceptHeader, acceptTextPlain)
handler.ServeHTTP(writer, request)
@ -311,7 +311,7 @@ func TestHandlerMaxRequestsInFlight(t *testing.T) {
w1 := httptest.NewRecorder()
w2 := httptest.NewRecorder()
w3 := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
request.Header.Add(acceptHeader, acceptTextPlain)
c := blockingCollector{Block: make(chan struct{}), CollectStarted: make(chan struct{}, 1)}
@ -348,7 +348,7 @@ func TestHandlerTimeout(t *testing.T) {
handler := HandlerFor(reg, HandlerOpts{Timeout: time.Millisecond})
w := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
request.Header.Add("Accept", "test/plain")
c := blockingCollector{Block: make(chan struct{}), CollectStarted: make(chan struct{}, 1)}
@ -372,7 +372,7 @@ func TestInstrumentMetricHandlerWithCompression(t *testing.T) {
handler := InstrumentMetricHandler(reg, HandlerForTransactional(mReg, HandlerOpts{DisableCompression: false}))
compression := Zstd
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
request.Header.Add(acceptHeader, acceptTextPlain)
request.Header.Add(acceptEncodingHeader, string(compression))
@ -533,7 +533,7 @@ func TestNegotiateEncodingWriter(t *testing.T) {
}
for _, test := range testCases {
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
request.Header.Add(acceptEncodingHeader, test.acceptEncoding)
rr := httptest.NewRecorder()
_, encodingHeader, _, err := negotiateEncodingWriter(request, rr, test.offeredCompressions)
@ -631,7 +631,7 @@ func BenchmarkCompression(b *testing.B) {
b.Run(benchmark.name+"_"+size.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
request.Header.Add(acceptEncodingHeader, benchmark.compressionType)
handler.ServeHTTP(writer, request)
}

View File

@ -223,7 +223,7 @@ func TestClientMiddlewareAPI_WithRequestContext(t *testing.T) {
}))
defer backend.Close()
req, err := http.NewRequest("GET", backend.URL, nil)
req, err := http.NewRequest(http.MethodGet, backend.URL, nil)
if err != nil {
t.Fatalf("%v", err)
}
@ -276,7 +276,7 @@ func TestClientMiddlewareAPIWithRequestContextTimeout(t *testing.T) {
}))
defer backend.Close()
req, err := http.NewRequest("GET", backend.URL, nil)
req, err := http.NewRequest(http.MethodGet, backend.URL, nil)
if err != nil {
t.Fatalf("%v", err)
}

View File

@ -418,7 +418,7 @@ func TestMiddlewareAPI(t *testing.T) {
_, _ = w.Write([]byte("OK"))
})
r, _ := http.NewRequest("GET", "www.example.com", nil)
r, _ := http.NewRequest(http.MethodGet, "www.example.com", nil)
w := httptest.NewRecorder()
chain.ServeHTTP(w, r)
@ -432,7 +432,7 @@ func TestMiddlewareAPI_WithExemplars(t *testing.T) {
_, _ = w.Write([]byte("OK"))
}, WithExemplarFromContext(func(_ context.Context) prometheus.Labels { return exemplar }))
r, _ := http.NewRequest("GET", "www.example.com", nil)
r, _ := http.NewRequest(http.MethodGet, "www.example.com", nil)
w := httptest.NewRecorder()
chain.ServeHTTP(w, r)

387
prometheus/promsafe/safe.go Normal file
View File

@ -0,0 +1,387 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package promsafe provides safe labeling - strongly typed labels in prometheus metrics.
// Enjoy promsafe as you wish!
package promsafe
import (
"fmt"
"reflect"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
//
// promsafe configuration: promauto-compatibility, etc
//
// factory stands for a global promauto.Factory to be used (if any)
var factory *promauto.Factory
// SetupGlobalPromauto sets a global promauto.Factory to be used for all promsafe metrics.
// This means that each promsafe.New* call will use this promauto.Factory.
func SetupGlobalPromauto(factoryArg ...promauto.Factory) {
if len(factoryArg) == 0 {
f := promauto.With(prometheus.DefaultRegisterer)
factory = &f
} else {
f := factoryArg[0]
factory = &f
}
}
// CustomLabelsProvider is an interface that allows to convert anything to a prometheus.Labels
// It allows to provide your own FAST implementation of Struct->prometheus.Labels conversion
// without using reflection.
type CustomLabelsProvider interface {
ToPrometheusLabels() prometheus.Labels
}
// CustomLabelNamesProvider is an interface that allows to convert anything to an order list of label names
// Result of it is considered to be when registering new metric. As we need proper ORDERED list of label names
// It allows to provide your own FAST implementation of Struct->[]string conversion without any reflection
type CustomLabelNamesProvider interface {
ToLabelNames() []string
}
// promsafeTag is the tag name used for promsafe labels inside structs.
// The tag is optional, as if not present, field is used with snake_cased FieldName.
// It's useful to use a tag when you want to override the default naming or exclude a field from the metric.
var promsafeTag = "promsafe"
// SetPromsafeTag sets the tag name used for promsafe labels inside structs.
func SetPromsafeTag(tag string) {
promsafeTag = tag
}
// labelsProviderMarker is a marker interface for enforcing type-safety of StructLabelProvider.
type labelsProviderMarker interface {
labelsProviderMarker()
}
// StructLabelProvider should be embedded in any struct that serves as a label provider.
type StructLabelProvider struct{}
var _ labelsProviderMarker = (*StructLabelProvider)(nil)
func (s StructLabelProvider) labelsProviderMarker() {
panic("labelsProviderMarker interface method should never be called")
}
// newEmptyLabels creates a new empty labels instance of type T
// It's a bit tricky as we want to support both structs and pointers to structs
// e.g. &MyLabels{StructLabelProvider} or MyLabels{StructLabelProvider}
func newEmptyLabels[T labelsProviderMarker]() T {
var emptyLabels T
// Let's Support both Structs or Pointer to Structs given as T
val := reflect.ValueOf(&emptyLabels).Elem()
if val.Kind() == reflect.Ptr {
val.Set(reflect.New(val.Type().Elem()))
}
return emptyLabels
}
// NewCounterVecT creates a new CounterVecT with type-safe labels.
func NewCounterVecT[T labelsProviderMarker](opts prometheus.CounterOpts) *CounterVecT[T] {
emptyLabels := newEmptyLabels[T]()
var inner *prometheus.CounterVec
if factory != nil {
inner = factory.NewCounterVec(opts, extractLabelNames(emptyLabels))
} else {
inner = prometheus.NewCounterVec(opts, extractLabelNames(emptyLabels))
}
return &CounterVecT[T]{inner: inner}
}
// CounterVecT is a wrapper around prometheus.CounterVec that allows type-safe labels.
type CounterVecT[T labelsProviderMarker] struct {
inner *prometheus.CounterVec
}
// GetMetricWithLabelValues covers prometheus.CounterVec.GetMetricWithLabelValues
// Deprecated: Use GetMetricWith() instead. We can't provide a []string safe implementation in promsafe
func (c *CounterVecT[T]) GetMetricWithLabelValues(lvs ...string) (prometheus.Counter, error) {
panic("There can't be a SAFE GetMetricWithLabelValues(). Use GetMetricWith() instead")
}
// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels.
func (c *CounterVecT[T]) GetMetricWith(labels T) (prometheus.Counter, error) {
return c.inner.GetMetricWith(extractLabelsWithValues(labels))
}
// WithLabelValues covers like prometheus.CounterVec.WithLabelValues.
// Deprecated: Use With() instead. We can't provide a []string safe implementation in promsafe
func (c *CounterVecT[T]) WithLabelValues(lvs ...string) prometheus.Counter {
panic("There can't be a SAFE WithLabelValues(). Use With() instead")
}
// With behaves like prometheus.CounterVec.With but with type-safe labels.
func (c *CounterVecT[T]) With(labels T) prometheus.Counter {
return c.inner.With(extractLabelsWithValues(labels))
}
// CurryWith behaves like prometheus.CounterVec.CurryWith but with type-safe labels.
// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried.
func (c *CounterVecT[T]) CurryWith(labels T) (*CounterVecT[T], error) {
curriedInner, err := c.inner.CurryWith(extractLabelsWithValues(labels))
if err != nil {
return nil, err
}
c.inner = curriedInner
return c, nil
}
// MustCurryWith behaves like prometheus.CounterVec.MustCurryWith but with type-safe labels.
// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried.
func (c *CounterVecT[T]) MustCurryWith(labels T) *CounterVecT[T] {
c.inner = c.inner.MustCurryWith(extractLabelsWithValues(labels))
return c
}
// Unsafe returns the underlying prometheus.CounterVec
// it's used to call any other method of prometheus.CounterVec that doesn't require type-safe labels
func (c *CounterVecT[T]) Unsafe() *prometheus.CounterVec {
return c.inner
}
// NewCounterT simply creates a new prometheus.Counter.
// As it doesn't have any labels, it's already type-safe.
// We keep this method just for consistency and interface fulfillment.
func NewCounterT(opts prometheus.CounterOpts) prometheus.Counter {
return prometheus.NewCounter(opts)
}
// NewCounterFuncT simply creates a new prometheus.CounterFunc.
// As it doesn't have any labels, it's already type-safe.
// We keep this method just for consistency and interface fulfillment.
func NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
return prometheus.NewCounterFunc(opts, function)
}
//
// Shorthand for Metrics with a single label
//
// NewCounterVecT1 creates a new CounterVecT with the only single label
func NewCounterVecT1(opts prometheus.CounterOpts, labelName string) *CounterVecT1 {
var inner *prometheus.CounterVec
if factory != nil {
inner = factory.NewCounterVec(opts, []string{labelName})
} else {
inner = prometheus.NewCounterVec(opts, []string{labelName})
}
return &CounterVecT1{inner: inner, labelName: labelName}
}
// CounterVecT1 is a wrapper around prometheus.CounterVec that allows a single type-safe label.
type CounterVecT1 struct {
labelName string
inner *prometheus.CounterVec
}
// GetMetricWithLabelValues behaves like prometheus.CounterVec.GetMetricWithLabelValues but with type-safe labels.
func (c *CounterVecT1) GetMetricWithLabelValues(labelValue string) (prometheus.Counter, error) {
return c.inner.GetMetricWithLabelValues(labelValue)
}
// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels.
// We keep this function for consistency. Actually is useless and it's the same as GetMetricWithLabelValues
func (c *CounterVecT1) GetMetricWith(labelValue string) (prometheus.Counter, error) {
return c.inner.GetMetricWith(prometheus.Labels{c.labelName: labelValue})
}
// WithLabelValues behaves like prometheus.CounterVec.WithLabelValues but with type-safe labels.
func (c *CounterVecT1) WithLabelValues(labelValue string) prometheus.Counter {
return c.inner.WithLabelValues(labelValue)
}
// With behaves like prometheus.CounterVec.With but with type-safe labels.
// We keep this function for consistency. Actually is useless and it's the same as WithLabelValues
func (c *CounterVecT1) With(labelValue string) prometheus.Counter {
return c.inner.With(prometheus.Labels{c.labelName: labelValue})
}
// CurryWith behaves like prometheus.CounterVec.CurryWith but with type-safe labels.
// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried.
func (c *CounterVecT1) CurryWith(labelValue string) (*CounterVecT1, error) {
curriedInner, err := c.inner.CurryWith(prometheus.Labels{c.labelName: labelValue})
if err != nil {
return nil, err
}
c.inner = curriedInner
return c, nil
}
// MustCurryWith behaves like prometheus.CounterVec.MustCurryWith but with type-safe labels.
// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried.
func (c *CounterVecT1) MustCurryWith(labelValue string) *CounterVecT1 {
c.inner = c.inner.MustCurryWith(prometheus.Labels{c.labelName: labelValue})
return c
}
// Unsafe returns the underlying prometheus.CounterVec
// it's used to call any other method of prometheus.CounterVec that doesn't require type-safe labels
func (c *CounterVecT1) Unsafe() *prometheus.CounterVec {
return c.inner
}
//
// Promauto compatibility
//
// Factory is a promauto-like factory that allows type-safe labels.
// We have to duplicate promauto.Factory logic here, because promauto.Factory's registry is private.
type Factory[T labelsProviderMarker] struct {
r prometheus.Registerer
}
// WithAuto is a helper function that allows to use promauto.With with promsafe.With
func WithAuto[T labelsProviderMarker](r prometheus.Registerer) Factory[T] {
return Factory[T]{r: r}
}
// NewCounterVecT works like promauto.NewCounterVec but with type-safe labels
func (f Factory[T]) NewCounterVecT(opts prometheus.CounterOpts) *CounterVecT[T] {
c := NewCounterVecT[T](opts)
if f.r != nil {
f.r.MustRegister(c.inner)
}
return c
}
// NewCounterT wraps promauto.NewCounter.
// As it doesn't require any labels, it's already type-safe, and we keep it for consistency.
func (f Factory[T]) NewCounterT(opts prometheus.CounterOpts) prometheus.Counter {
return promauto.With(f.r).NewCounter(opts)
}
// NewCounterFuncT wraps promauto.NewCounterFunc.
// As it doesn't require any labels, it's already type-safe, and we keep it for consistency.
func (f Factory[T]) NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
return promauto.With(f.r).NewCounterFunc(opts, function)
}
// TODO: we can't use Factory with NewCounterT1. If we need, then we need a new type-less Factory
//
// Helpers
//
// extractLabelsWithValues extracts labels names+values from a given labelsProviderMarker (parent instance of a StructLabelProvider)
func extractLabelsWithValues(labelProvider labelsProviderMarker) prometheus.Labels {
if any(labelProvider) == nil {
return nil
}
if clp, ok := labelProvider.(CustomLabelsProvider); ok {
return clp.ToPrometheusLabels()
}
// Here, then, it can be only a struct, that is a parent of StructLabelProvider
return extractLabelFromStruct(labelProvider)
}
// extractLabelNames extracts labels names from a given labelsProviderMarker (parent instance of aStructLabelProvider)
func extractLabelNames(labelProvider labelsProviderMarker) []string {
if any(labelProvider) == nil {
return nil
}
// If custom implementation is done, just do it
if clp, ok := labelProvider.(CustomLabelNamesProvider); ok {
return clp.ToLabelNames()
}
// Fallback to slow implementation via reflect
// We'll return label values in order of fields in the struct
val := reflect.Indirect(reflect.ValueOf(labelProvider))
typ := val.Type()
labelNames := make([]string, 0, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Anonymous {
continue
}
var fieldName string
if ourTag := field.Tag.Get(promsafeTag); ourTag == "-" {
continue
} else if ourTag != "" {
fieldName = ourTag
} else {
fieldName = toSnakeCase(field.Name)
}
labelNames = append(labelNames, fieldName)
}
return labelNames
}
// extractLabelFromStruct extracts labels names+values from a given StructLabelProvider
func extractLabelFromStruct(structWithLabels any) prometheus.Labels {
labels := prometheus.Labels{}
val := reflect.Indirect(reflect.ValueOf(structWithLabels))
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Anonymous {
continue
}
var labelName string
if ourTag := field.Tag.Get(promsafeTag); ourTag != "" {
if ourTag == "-" { // tag="-" means "skip this field"
continue
}
labelName = ourTag
} else {
labelName = toSnakeCase(field.Name)
}
labels[labelName] = stringifyLabelValue(val.Field(i))
}
return labels
}
// stringifyLabelValue makes up a valid string value from a given field's value
// It's used ONLY in fallback reflect mode
// Field value might be a pointer, that's why we do reflect.Indirect()
// Note: in future we can handle default values here as well
func stringifyLabelValue(v reflect.Value) string {
return fmt.Sprintf("%v", reflect.Indirect(v).Interface())
}
// Convert struct field names to snake_case for Prometheus label compliance.
func toSnakeCase(s string) string {
s = strings.TrimSpace(s)
var result []rune
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result = append(result, '_')
}
result = append(result, r)
}
return strings.ToLower(string(result))
}

View File

@ -0,0 +1,197 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package promsafe_test
import (
"log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promsafe"
)
// Important: This is not a test file. These are only examples!
// These can be considered smoke tests, but nothing more.
// TODO: Write real tests
func ExampleNewCounterVecT_multiple_labels_manual() {
// Manually registering with multiple labels
type MyCounterLabels struct {
promsafe.StructLabelProvider
EventType string
Success bool
Position uint8 // yes, it's a number, but be careful with high-cardinality labels
ShouldNotBeUsed string `promsafe:"-"`
}
c := promsafe.NewCounterVecT[MyCounterLabels](prometheus.CounterOpts{
Name: "items_counted_detailed",
})
// Manually register the counter
if err := prometheus.Register(c.Unsafe()); err != nil {
log.Fatal("could not register: ", err.Error())
}
// and now, because of generics we can call Inc() with filled struct of labels:
counter := c.With(MyCounterLabels{
EventType: "reservation", Success: true, Position: 1,
})
counter.Inc()
// Output:
}
func ExampleNewCounterVecT_promauto_migrated() {
// Examples on how to migrate from promauto to promsafe
// When promauto was using a custom factory with custom registry
myReg := prometheus.NewRegistry()
counterOpts := prometheus.CounterOpts{
Name: "items_counted_detailed_auto",
}
// Old unsafe code
// promauto.With(myReg).NewCounterVec(counterOpts, []string{"event_type", "source"})
// becomes:
type MyLabels struct {
promsafe.StructLabelProvider
EventType string
Source string
}
c := promsafe.WithAuto[MyLabels](myReg).NewCounterVecT(counterOpts)
c.With(MyLabels{
EventType: "reservation", Source: "source1",
}).Inc()
// Output:
}
func ExampleNewCounterVecT_promauto_global_migrated() {
// Examples on how to migrate from promauto to promsafe
// when promauto public API was used (with default registry)
// Setup so every NewCounter* call will use default registry
// like promauto does
// Note: it actually accepts other registry to become a default one
promsafe.SetupGlobalPromauto()
defer func() {
// cleanup for other examples
promsafe.SetupGlobalPromauto(promauto.With(nil))
}()
counterOpts := prometheus.CounterOpts{
Name: "items_counted_detailed_auto_global",
}
// Old code:
//c := promauto.NewCounterVec(counterOpts, []string{"status", "source"})
//c.With(prometheus.Labels{
// "status": "active",
// "source": "source1",
//}).Inc()
// becomes:
type MyLabels struct {
promsafe.StructLabelProvider
Status string
Source string
}
c := promsafe.NewCounterVecT[*MyLabels](counterOpts)
c.With(&MyLabels{
Status: "active", Source: "source1",
}).Inc()
// Output:
}
func ExampleNewCounterVecT_pointer_to_labels_promauto() {
// It's possible to use pointer to labels struct
myReg := prometheus.NewRegistry()
counterOpts := prometheus.CounterOpts{
Name: "items_counted_detailed_ptr",
}
type MyLabels struct {
promsafe.StructLabelProvider
EventType string
Source string
}
c := promsafe.WithAuto[*MyLabels](myReg).NewCounterVecT(counterOpts)
c.With(&MyLabels{
EventType: "reservation", Source: "source1",
}).Inc()
// Output:
}
// FastMyLabels is a struct that will have a custom method that converts to prometheus.Labels
type FastMyLabels struct {
promsafe.StructLabelProvider
EventType string
Source string
}
// ToPrometheusLabels does a superfast conversion to labels. So no reflection is involved.
func (f FastMyLabels) ToPrometheusLabels() prometheus.Labels {
return prometheus.Labels{"event_type": f.EventType, "source": f.Source}
}
// ToLabelNames does a superfast label names list. So no reflection is involved.
func (f FastMyLabels) ToLabelNames() []string {
return []string{"event_type", "source"}
}
func ExampleNewCounterVecT_fast_safe_labels_provider() {
// It's possible to use pointer to labels struct
myReg := prometheus.NewRegistry()
counterOpts := prometheus.CounterOpts{
Name: "items_counted_fast",
}
c := promsafe.WithAuto[FastMyLabels](myReg).NewCounterVecT(counterOpts)
c.With(FastMyLabels{
EventType: "reservation", Source: "source1",
}).Inc()
// Output:
}
func ExampleNewCounterVecT_single_label_manual() {
// Manually registering with a single label
// Example of usage of shorthand: no structs no generics, but one string only
c := promsafe.NewCounterVecT1(prometheus.CounterOpts{
Name: "items_counted_by_status",
}, "status")
// Manually register the counter
if err := prometheus.Register(c.Unsafe()); err != nil {
log.Fatal("could not register: ", err.Error())
}
c.With("active").Inc()
// Output:
}

View File

@ -27,6 +27,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"sync"
"testing"
"time"
@ -713,7 +714,7 @@ collected metric "broken_metric" { label:<name:"foo" value:"bar" > label:<name:"
}
writer := httptest.NewRecorder()
handler := promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{})
request, _ := http.NewRequest("GET", "/", nil)
request, _ := http.NewRequest(http.MethodGet, "/", nil)
for key, value := range scenario.headers {
request.Header.Add(key, value)
}
@ -1282,7 +1283,7 @@ func ExampleRegistry_grouping() {
ConstLabels: prometheus.Labels{
// Generate a label unique to this worker so its metric doesn't
// collide with the metrics from other workers.
"worker_id": fmt.Sprintf("%d", workerID),
"worker_id": strconv.Itoa(workerID),
},
})
workerReg.MustRegister(workTime)

View File

@ -471,13 +471,9 @@ func (s *noObjectivesSummary) Observe(v float64) {
n := atomic.AddUint64(&s.countAndHotIdx, 1)
hotCounts := s.counts[n>>63]
for {
oldBits := atomic.LoadUint64(&hotCounts.sumBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
if atomic.CompareAndSwapUint64(&hotCounts.sumBits, oldBits, newBits) {
break
}
}
atomicUpdateFloat(&hotCounts.sumBits, func(oldVal float64) float64 {
return oldVal + v
})
// Increment count last as we take it as a signal that the observation
// is complete.
atomic.AddUint64(&hotCounts.count, 1)
@ -519,14 +515,13 @@ func (s *noObjectivesSummary) Write(out *dto.Metric) error {
// Finally add all the cold counts to the new hot counts and reset the cold counts.
atomic.AddUint64(&hotCounts.count, count)
atomic.StoreUint64(&coldCounts.count, 0)
for {
oldBits := atomic.LoadUint64(&hotCounts.sumBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + sum.GetSampleSum())
if atomic.CompareAndSwapUint64(&hotCounts.sumBits, oldBits, newBits) {
atomic.StoreUint64(&coldCounts.sumBits, 0)
break
}
}
// Use atomicUpdateFloat to update hotCounts.sumBits atomically.
atomicUpdateFloat(&hotCounts.sumBits, func(oldVal float64) float64 {
return oldVal + sum.GetSampleSum()
})
atomic.StoreUint64(&coldCounts.sumBits, 0)
return nil
}

View File

@ -14,6 +14,7 @@
package promlint_test
import (
"errors"
"fmt"
"reflect"
"strings"
@ -733,7 +734,7 @@ request_duration_seconds{httpService="foo"} 10
func TestLintUnitAbbreviations(t *testing.T) {
genTest := func(n string) test {
return test{
name: fmt.Sprintf("%s with abbreviated unit", n),
name: n + " with abbreviated unit",
in: fmt.Sprintf(`
# HELP %s Test metric.
# TYPE %s gauge
@ -820,7 +821,7 @@ mc_something_total 10
prefixValidation := func(mf *dto.MetricFamily) []error {
if !strings.HasPrefix(mf.GetName(), "memcached_") {
return []error{fmt.Errorf("expected metric name to start with 'memcached_'")}
return []error{errors.New("expected metric name to start with 'memcached_'")}
}
return nil
}

View File

@ -14,7 +14,7 @@
package validations
import (
"fmt"
"errors"
"reflect"
dto "github.com/prometheus/client_model/go"
@ -27,7 +27,7 @@ func LintDuplicateMetric(mf *dto.MetricFamily) []error {
for i, m := range mf.Metric {
for _, k := range mf.Metric[i+1:] {
if reflect.DeepEqual(m.Label, k.Label) {
problems = append(problems, fmt.Errorf("metric not unique"))
problems = append(problems, errors.New("metric not unique"))
break
}
}

View File

@ -14,7 +14,6 @@
package prometheus
import (
"fmt"
"testing"
"time"
@ -51,7 +50,7 @@ func TestNewConstMetricInvalidLabelValues(t *testing.T) {
expectPanic(t, func() {
MustNewConstMetric(metricDesc, CounterValue, 0.3, "\xFF")
}, fmt.Sprintf("WithLabelValues: expected panic because: %s", test.desc))
}, "WithLabelValues: expected panic because: "+test.desc)
if _, err := NewConstMetric(metricDesc, CounterValue, 0.3, "\xFF"); err == nil {
t.Errorf("NewConstMetric: expected error because: %s", test.desc)

View File

@ -16,6 +16,7 @@ package prometheus
import (
"fmt"
"reflect"
"strconv"
"testing"
dto "github.com/prometheus/client_model/go"
@ -291,7 +292,7 @@ func testMetricVec(t *testing.T, vec *GaugeVec) {
expected := map[[2]string]int{}
for i := 0; i < 1000; i++ {
pair[0], pair[1] = fmt.Sprint(i%4), fmt.Sprint(i%5) // Varying combinations multiples.
pair[0], pair[1] = strconv.Itoa(i%4), strconv.Itoa(i%5) // Varying combinations multiples.
expected[pair]++
vec.WithLabelValues(pair[0], pair[1]).Inc()
@ -363,7 +364,7 @@ func testConstrainedMetricVec(t *testing.T, vec *GaugeVec, constrain func(string
expected := map[[2]string]int{}
for i := 0; i < 1000; i++ {
pair[0], pair[1] = fmt.Sprint(i%4), fmt.Sprint(i%5) // Varying combinations multiples.
pair[0], pair[1] = strconv.Itoa(i%4), strconv.Itoa(i%5) // Varying combinations multiples.
expected[[2]string{pair[0], constrain(pair[1])}]++
vec.WithLabelValues(pair[0], pair[1]).Inc()