diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile
index d645db5de1..2370ec5f5c 100644
--- a/.gitpod.Dockerfile
+++ b/.gitpod.Dockerfile
@@ -1,15 +1,33 @@
FROM gitpod/workspace-full
+# Set Node.js version as an environment variable.
ENV CUSTOM_NODE_VERSION=16
-ENV CUSTOM_GO_VERSION=1.19
-ENV GOPATH=$HOME/go-packages
-ENV GOROOT=$HOME/go
-ENV PATH=$GOROOT/bin:$GOPATH/bin:$PATH
+# Install and use the specified Node.js version via nvm.
RUN bash -c ". .nvm/nvm.sh && nvm install ${CUSTOM_NODE_VERSION} && nvm use ${CUSTOM_NODE_VERSION} && nvm alias default ${CUSTOM_NODE_VERSION}"
+# Ensure nvm uses the default Node.js version in all new shells.
RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix
-RUN curl -fsSL https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar xzs \
- && printf '%s\n' 'export GOPATH=/workspace/go' \
- 'export PATH=$GOPATH/bin:$PATH' > $HOME/.bashrc.d/300-go
+# Remove any existing Go installation in $HOME path.
+RUN rm -rf $HOME/go $HOME/go-packages
+
+# Export go environment variables.
+RUN echo "export GOPATH=/workspace/go" >> ~/.bashrc.d/300-go && \
+ echo "export GOBIN=\$GOPATH/bin" >> ~/.bashrc.d/300-go && \
+ echo "export GOROOT=${HOME}/go" >> ~/.bashrc.d/300-go && \
+ echo "export PATH=\$GOROOT/bin:\$GOBIN:\$PATH" >> ~/.bashrc
+
+# Reload the environment variables to ensure go environment variables are
+# available in subsequent commands.
+RUN bash -c "source ~/.bashrc && source ~/.bashrc.d/300-go"
+
+# Fetch the Go version dynamically from the Prometheus go.mod file and Install Go in $HOME path.
+RUN export CUSTOM_GO_VERSION=$(curl -sSL "https://raw.githubusercontent.com/prometheus/prometheus/main/go.mod" | awk '/^go/{print $2".0"}') && \
+ curl -fsSL "https://dl.google.com/go/go${CUSTOM_GO_VERSION}.linux-amd64.tar.gz" | \
+ tar -xz -C $HOME
+
+# Fetch the goyacc parser version dynamically from the Prometheus Makefile
+# and install it globally in $GOBIN path.
+RUN GOYACC_VERSION=$(curl -fsSL "https://raw.githubusercontent.com/prometheus/prometheus/main/Makefile" | awk -F'=' '/GOYACC_VERSION \?=/{gsub(/ /, "", $2); print $2}') && \
+ go install "golang.org/x/tools/cmd/goyacc@${GOYACC_VERSION}"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7687826ba4..9b1b286ccf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,7 +42,12 @@ go build ./cmd/prometheus/
make test # Make sure all the tests pass before you commit and push :)
```
-We use [`golangci-lint`](https://github.com/golangci/golangci-lint) for linting the code. If it reports an issue and you think that the warning needs to be disregarded or is a false-positive, you can add a special comment `//nolint:linter1[,linter2,...]` before the offending line. Use this sparingly though, fixing the code to comply with the linter's recommendation is in general the preferred course of action.
+To run a collection of Go linters through [`golangci-lint`](https://github.com/golangci/golangci-lint), do:
+```bash
+make lint
+```
+
+If it reports an issue and you think that the warning needs to be disregarded or is a false-positive, you can add a special comment `//nolint:linter1[,linter2,...]` before the offending line. Use this sparingly though, fixing the code to comply with the linter's recommendation is in general the preferred course of action. See [this section of the golangci-lint documentation](https://golangci-lint.run/usage/false-positives/#nolint-directive) for more information.
All our issues are regularly tagged so that you can also filter down the issues involving the components you want to work on. For our labeling policy refer [the wiki page](https://github.com/prometheus/prometheus/wiki/Label-Names-and-Descriptions).
diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go
index 8218ffb18d..e250a95c82 100644
--- a/cmd/prometheus/main.go
+++ b/cmd/prometheus/main.go
@@ -418,7 +418,7 @@ func main() {
serverOnlyFlag(a, "rules.alert.resend-delay", "Minimum amount of time to wait before resending an alert to Alertmanager.").
Default("1m").SetValue(&cfg.resendDelay)
- serverOnlyFlag(a, "rules.max-concurrent-evals", "Global concurrency limit for independent rules that can run concurrently.").
+ serverOnlyFlag(a, "rules.max-concurrent-evals", "Global concurrency limit for independent rules that can run concurrently. When set, \"query.max-concurrency\" may need to be adjusted accordingly.").
Default("4").Int64Var(&cfg.maxConcurrentEvals)
a.Flag("scrape.adjust-timestamps", "Adjust scrape timestamps by up to `scrape.timestamp-tolerance` to align them to the intended schedule. See https://github.com/prometheus/prometheus/issues/7846 for more context. Experimental. This flag will be removed in a future release.").
diff --git a/cmd/promtool/backfill.go b/cmd/promtool/backfill.go
index 601c3ced9f..79db428c71 100644
--- a/cmd/promtool/backfill.go
+++ b/cmd/promtool/backfill.go
@@ -88,7 +88,7 @@ func createBlocks(input []byte, mint, maxt, maxBlockDuration int64, maxSamplesIn
blockDuration := getCompatibleBlockDuration(maxBlockDuration)
mint = blockDuration * (mint / blockDuration)
- db, err := tsdb.OpenDBReadOnly(outputDir, nil)
+ db, err := tsdb.OpenDBReadOnly(outputDir, "", nil)
if err != nil {
return err
}
diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go
index c0484adcc0..e1d275e97e 100644
--- a/cmd/promtool/main.go
+++ b/cmd/promtool/main.go
@@ -235,12 +235,14 @@ func main() {
tsdbDumpCmd := tsdbCmd.Command("dump", "Dump samples from a TSDB.")
dumpPath := tsdbDumpCmd.Arg("db path", "Database path (default is "+defaultDBPath+").").Default(defaultDBPath).String()
+ dumpSandboxDirRoot := tsdbDumpCmd.Flag("sandbox-dir-root", "Root directory where a sandbox directory would be created in case WAL replay generates chunks. The sandbox directory is cleaned up at the end.").Default(defaultDBPath).String()
dumpMinTime := tsdbDumpCmd.Flag("min-time", "Minimum timestamp to dump.").Default(strconv.FormatInt(math.MinInt64, 10)).Int64()
dumpMaxTime := tsdbDumpCmd.Flag("max-time", "Maximum timestamp to dump.").Default(strconv.FormatInt(math.MaxInt64, 10)).Int64()
dumpMatch := tsdbDumpCmd.Flag("match", "Series selector. Can be specified multiple times.").Default("{__name__=~'(?s:.*)'}").Strings()
- tsdbDumpOpenMetricsCmd := tsdbCmd.Command("dump-openmetrics", "[Experimental] Dump samples from a TSDB into OpenMetrics format. Native histograms are not dumped.")
+ tsdbDumpOpenMetricsCmd := tsdbCmd.Command("dump-openmetrics", "[Experimental] Dump samples from a TSDB into OpenMetrics text format, excluding native histograms and staleness markers, which are not representable in OpenMetrics.")
dumpOpenMetricsPath := tsdbDumpOpenMetricsCmd.Arg("db path", "Database path (default is "+defaultDBPath+").").Default(defaultDBPath).String()
+ dumpOpenMetricsSandboxDirRoot := tsdbDumpOpenMetricsCmd.Flag("sandbox-dir-root", "Root directory where a sandbox directory would be created in case WAL replay generates chunks. The sandbox directory is cleaned up at the end.").Default(defaultDBPath).String()
dumpOpenMetricsMinTime := tsdbDumpOpenMetricsCmd.Flag("min-time", "Minimum timestamp to dump.").Default(strconv.FormatInt(math.MinInt64, 10)).Int64()
dumpOpenMetricsMaxTime := tsdbDumpOpenMetricsCmd.Flag("max-time", "Maximum timestamp to dump.").Default(strconv.FormatInt(math.MaxInt64, 10)).Int64()
dumpOpenMetricsMatch := tsdbDumpOpenMetricsCmd.Flag("match", "Series selector. Can be specified multiple times.").Default("{__name__=~'(?s:.*)'}").Strings()
@@ -396,9 +398,9 @@ func main() {
os.Exit(checkErr(listBlocks(*listPath, *listHumanReadable)))
case tsdbDumpCmd.FullCommand():
- os.Exit(checkErr(dumpSamples(ctx, *dumpPath, *dumpMinTime, *dumpMaxTime, *dumpMatch, formatSeriesSet)))
+ os.Exit(checkErr(dumpSamples(ctx, *dumpPath, *dumpSandboxDirRoot, *dumpMinTime, *dumpMaxTime, *dumpMatch, formatSeriesSet)))
case tsdbDumpOpenMetricsCmd.FullCommand():
- os.Exit(checkErr(dumpSamples(ctx, *dumpOpenMetricsPath, *dumpOpenMetricsMinTime, *dumpOpenMetricsMaxTime, *dumpOpenMetricsMatch, formatSeriesSetOpenMetrics)))
+ os.Exit(checkErr(dumpSamples(ctx, *dumpOpenMetricsPath, *dumpOpenMetricsSandboxDirRoot, *dumpOpenMetricsMinTime, *dumpOpenMetricsMaxTime, *dumpOpenMetricsMatch, formatSeriesSetOpenMetrics)))
// TODO(aSquare14): Work on adding support for custom block size.
case openMetricsImportCmd.FullCommand():
os.Exit(backfillOpenMetrics(*importFilePath, *importDBPath, *importHumanReadable, *importQuiet, *maxBlockDuration))
diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go
index 6868102fa3..2ed7244b1c 100644
--- a/cmd/promtool/tsdb.go
+++ b/cmd/promtool/tsdb.go
@@ -338,7 +338,7 @@ func readPrometheusLabels(r io.Reader, n int) ([]labels.Labels, error) {
}
func listBlocks(path string, humanReadable bool) error {
- db, err := tsdb.OpenDBReadOnly(path, nil)
+ db, err := tsdb.OpenDBReadOnly(path, "", nil)
if err != nil {
return err
}
@@ -393,7 +393,7 @@ func getFormatedBytes(bytes int64, humanReadable bool) string {
}
func openBlock(path, blockID string) (*tsdb.DBReadOnly, tsdb.BlockReader, error) {
- db, err := tsdb.OpenDBReadOnly(path, nil)
+ db, err := tsdb.OpenDBReadOnly(path, "", nil)
if err != nil {
return nil, nil, err
}
@@ -708,8 +708,8 @@ func analyzeCompaction(ctx context.Context, block tsdb.BlockReader, indexr tsdb.
type SeriesSetFormatter func(series storage.SeriesSet) error
-func dumpSamples(ctx context.Context, path string, mint, maxt int64, match []string, formatter SeriesSetFormatter) (err error) {
- db, err := tsdb.OpenDBReadOnly(path, nil)
+func dumpSamples(ctx context.Context, dbDir, sandboxDirRoot string, mint, maxt int64, match []string, formatter SeriesSetFormatter) (err error) {
+ db, err := tsdb.OpenDBReadOnly(dbDir, sandboxDirRoot, nil)
if err != nil {
return err
}
diff --git a/cmd/promtool/tsdb_test.go b/cmd/promtool/tsdb_test.go
index 70e8877659..75089b168b 100644
--- a/cmd/promtool/tsdb_test.go
+++ b/cmd/promtool/tsdb_test.go
@@ -64,6 +64,7 @@ func getDumpedSamples(t *testing.T, path string, mint, maxt int64, match []strin
err := dumpSamples(
context.Background(),
path,
+ t.TempDir(),
mint,
maxt,
match,
diff --git a/docs/command-line/prometheus.md b/docs/command-line/prometheus.md
index 93eaf251d0..aa9bf3bfb0 100644
--- a/docs/command-line/prometheus.md
+++ b/docs/command-line/prometheus.md
@@ -48,7 +48,7 @@ The Prometheus monitoring server
| --rules.alert.for-outage-tolerance | Max time to tolerate prometheus outage for restoring "for" state of alert. Use with server mode only. | `1h` |
| --rules.alert.for-grace-period | Minimum duration between alert and restored "for" state. This is maintained only for alerts with configured "for" time greater than grace period. Use with server mode only. | `10m` |
| --rules.alert.resend-delay | Minimum amount of time to wait before resending an alert to Alertmanager. Use with server mode only. | `1m` |
-| --rules.max-concurrent-evals | Global concurrency limit for independent rules that can run concurrently. Use with server mode only. | `4` |
+| --rules.max-concurrent-evals | Global concurrency limit for independent rules that can run concurrently. When set, "query.max-concurrency" may need to be adjusted accordingly. Use with server mode only. | `4` |
| --alertmanager.notification-queue-capacity | The capacity of the queue for pending Alertmanager notifications. Use with server mode only. | `10000` |
| --query.lookback-delta | The maximum lookback duration for retrieving metrics during expression evaluations and federation. Use with server mode only. | `5m` |
| --query.timeout | Maximum time a query may take before being aborted. Use with server mode only. | `2m` |
diff --git a/docs/command-line/promtool.md b/docs/command-line/promtool.md
index 3eceed48f2..443cd3f0cb 100644
--- a/docs/command-line/promtool.md
+++ b/docs/command-line/promtool.md
@@ -566,6 +566,7 @@ Dump samples from a TSDB.
| Flag | Description | Default |
| --- | --- | --- |
+| --sandbox-dir-root | Root directory where a sandbox directory would be created in case WAL replay generates chunks. The sandbox directory is cleaned up at the end. | `data/` |
| --min-time | Minimum timestamp to dump. | `-9223372036854775808` |
| --max-time | Maximum timestamp to dump. | `9223372036854775807` |
| --match | Series selector. Can be specified multiple times. | `{__name__=~'(?s:.*)'}` |
@@ -584,7 +585,7 @@ Dump samples from a TSDB.
##### `promtool tsdb dump-openmetrics`
-[Experimental] Dump samples from a TSDB into OpenMetrics format. Native histograms are not dumped.
+[Experimental] Dump samples from a TSDB into OpenMetrics text format, excluding native histograms and staleness markers, which are not representable in OpenMetrics.
@@ -592,6 +593,7 @@ Dump samples from a TSDB.
| Flag | Description | Default |
| --- | --- | --- |
+| --sandbox-dir-root | Root directory where a sandbox directory would be created in case WAL replay generates chunks. The sandbox directory is cleaned up at the end. | `data/` |
| --min-time | Minimum timestamp to dump. | `-9223372036854775808` |
| --max-time | Maximum timestamp to dump. | `9223372036854775807` |
| --match | Series selector. Can be specified multiple times. | `{__name__=~'(?s:.*)'}` |
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index bfd0312746..61e86988e1 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -1467,6 +1467,7 @@ For OVHcloud's [public cloud instances](https://www.ovhcloud.com/en/public-cloud
* `__meta_ovhcloud_dedicated_server_ipv6`: the IPv6 of the server
* `__meta_ovhcloud_dedicated_server_link_speed`: the link speed of the server
* `__meta_ovhcloud_dedicated_server_name`: the name of the server
+* `__meta_ovhcloud_dedicated_server_no_intervention`: whether datacenter intervention is disabled for the server
* `__meta_ovhcloud_dedicated_server_os`: the operating system of the server
* `__meta_ovhcloud_dedicated_server_rack`: the rack of the server
* `__meta_ovhcloud_dedicated_server_reverse`: the reverse DNS name of the server
diff --git a/docs/storage.md b/docs/storage.md
index 46bb7210e0..b66f2062af 100644
--- a/docs/storage.md
+++ b/docs/storage.md
@@ -197,6 +197,9 @@ or time-series database to Prometheus. To do so, the user must first convert the
source data into [OpenMetrics](https://openmetrics.io/) format, which is the
input format for the backfilling as described below.
+Note that native histograms and staleness markers are not supported by this
+procedure, as they cannot be represented in the OpenMetrics format.
+
### Usage
Backfilling can be used via the Promtool command line. Promtool will write the blocks
diff --git a/documentation/prometheus-mixin/config.libsonnet b/documentation/prometheus-mixin/config.libsonnet
index ab9079a5e3..70d46a2212 100644
--- a/documentation/prometheus-mixin/config.libsonnet
+++ b/documentation/prometheus-mixin/config.libsonnet
@@ -44,5 +44,10 @@
// The default refresh time for all dashboards, default to 60s
refresh: '60s',
},
+
+ // Opt-out of multi-cluster dashboards by overriding this.
+ showMultiCluster: true,
+ // The cluster label to infer the cluster name from.
+ clusterLabel: 'cluster',
},
}
diff --git a/documentation/prometheus-mixin/dashboards.libsonnet b/documentation/prometheus-mixin/dashboards.libsonnet
index efe53dbac9..2bdd168cc9 100644
--- a/documentation/prometheus-mixin/dashboards.libsonnet
+++ b/documentation/prometheus-mixin/dashboards.libsonnet
@@ -10,21 +10,32 @@ local template = grafana.template;
{
grafanaDashboards+:: {
'prometheus.json':
- g.dashboard(
+ local showMultiCluster = $._config.showMultiCluster;
+ local dashboard = g.dashboard(
'%(prefix)sOverview' % $._config.grafanaPrometheus
- )
- .addMultiTemplate('cluster', 'prometheus_build_info{%(prometheusSelector)s}' % $._config, 'cluster')
- .addMultiTemplate('job', 'prometheus_build_info{cluster=~"$cluster"}', 'job')
- .addMultiTemplate('instance', 'prometheus_build_info{cluster=~"$cluster", job=~"$job"}', 'instance')
+ );
+ local templatedDashboard = if showMultiCluster then
+ dashboard
+ .addMultiTemplate('cluster', 'prometheus_build_info{%(prometheusSelector)s}' % $._config, $._config.clusterLabel)
+ .addMultiTemplate('job', 'prometheus_build_info{cluster=~"$cluster"}', 'job')
+ .addMultiTemplate('instance', 'prometheus_build_info{cluster=~"$cluster", job=~"$job"}', 'instance')
+ else
+ dashboard
+ .addMultiTemplate('job', 'prometheus_build_info{%(prometheusSelector)s}' % $._config, 'job')
+ .addMultiTemplate('instance', 'prometheus_build_info{job=~"$job"}', 'instance');
+ templatedDashboard
.addRow(
g.row('Prometheus Stats')
.addPanel(
g.panel('Prometheus Stats') +
- g.tablePanel([
+ g.tablePanel(if showMultiCluster then [
'count by (cluster, job, instance, version) (prometheus_build_info{cluster=~"$cluster", job=~"$job", instance=~"$instance"})',
'max by (cluster, job, instance) (time() - process_start_time_seconds{cluster=~"$cluster", job=~"$job", instance=~"$instance"})',
+ ] else [
+ 'count by (job, instance, version) (prometheus_build_info{job=~"$job", instance=~"$instance"})',
+ 'max by (job, instance) (time() - process_start_time_seconds{job=~"$job", instance=~"$instance"})',
], {
- cluster: { alias: 'Cluster' },
+ cluster: { alias: if showMultiCluster then 'Cluster' else '' },
job: { alias: 'Job' },
instance: { alias: 'Instance' },
version: { alias: 'Version' },
@@ -37,12 +48,18 @@ local template = grafana.template;
g.row('Discovery')
.addPanel(
g.panel('Target Sync') +
- g.queryPanel('sum(rate(prometheus_target_sync_length_seconds_sum{cluster=~"$cluster",job=~"$job",instance=~"$instance"}[5m])) by (cluster, job, scrape_job, instance) * 1e3', '{{cluster}}:{{job}}:{{instance}}:{{scrape_job}}') +
+ g.queryPanel(if showMultiCluster then 'sum(rate(prometheus_target_sync_length_seconds_sum{cluster=~"$cluster",job=~"$job",instance=~"$instance"}[5m])) by (cluster, job, scrape_job, instance) * 1e3'
+ else 'sum(rate(prometheus_target_sync_length_seconds_sum{job=~"$job",instance=~"$instance"}[5m])) by (scrape_job) * 1e3',
+ if showMultiCluster then '{{cluster}}:{{job}}:{{instance}}:{{scrape_job}}'
+ else '{{scrape_job}}') +
{ yaxes: g.yaxes('ms') }
)
.addPanel(
g.panel('Targets') +
- g.queryPanel('sum by (cluster, job, instance) (prometheus_sd_discovered_targets{cluster=~"$cluster", job=~"$job",instance=~"$instance"})', '{{cluster}}:{{job}}:{{instance}}') +
+ g.queryPanel(if showMultiCluster then 'sum by (cluster, job, instance) (prometheus_sd_discovered_targets{cluster=~"$cluster", job=~"$job",instance=~"$instance"})'
+ else 'sum(prometheus_sd_discovered_targets{job=~"$job",instance=~"$instance"})',
+ if showMultiCluster then '{{cluster}}:{{job}}:{{instance}}'
+ else 'Targets') +
g.stack
)
)
@@ -50,29 +67,47 @@ local template = grafana.template;
g.row('Retrieval')
.addPanel(
g.panel('Average Scrape Interval Duration') +
- g.queryPanel('rate(prometheus_target_interval_length_seconds_sum{cluster=~"$cluster", job=~"$job",instance=~"$instance"}[5m]) / rate(prometheus_target_interval_length_seconds_count{cluster=~"$cluster", job=~"$job",instance=~"$instance"}[5m]) * 1e3', '{{cluster}}:{{job}}:{{instance}} {{interval}} configured') +
+ g.queryPanel(if showMultiCluster then 'rate(prometheus_target_interval_length_seconds_sum{cluster=~"$cluster", job=~"$job",instance=~"$instance"}[5m]) / rate(prometheus_target_interval_length_seconds_count{cluster=~"$cluster", job=~"$job",instance=~"$instance"}[5m]) * 1e3'
+ else 'rate(prometheus_target_interval_length_seconds_sum{job=~"$job",instance=~"$instance"}[5m]) / rate(prometheus_target_interval_length_seconds_count{job=~"$job",instance=~"$instance"}[5m]) * 1e3',
+ if showMultiCluster then '{{cluster}}:{{job}}:{{instance}} {{interval}} configured'
+ else '{{interval}} configured') +
{ yaxes: g.yaxes('ms') }
)
.addPanel(
g.panel('Scrape failures') +
- g.queryPanel([
+ g.queryPanel(if showMultiCluster then [
'sum by (cluster, job, instance) (rate(prometheus_target_scrapes_exceeded_body_size_limit_total{cluster=~"$cluster",job=~"$job",instance=~"$instance"}[1m]))',
'sum by (cluster, job, instance) (rate(prometheus_target_scrapes_exceeded_sample_limit_total{cluster=~"$cluster",job=~"$job",instance=~"$instance"}[1m]))',
'sum by (cluster, job, instance) (rate(prometheus_target_scrapes_sample_duplicate_timestamp_total{cluster=~"$cluster",job=~"$job",instance=~"$instance"}[1m]))',
'sum by (cluster, job, instance) (rate(prometheus_target_scrapes_sample_out_of_bounds_total{cluster=~"$cluster",job=~"$job",instance=~"$instance"}[1m]))',
'sum by (cluster, job, instance) (rate(prometheus_target_scrapes_sample_out_of_order_total{cluster=~"$cluster",job=~"$job",instance=~"$instance"}[1m]))',
- ], [
+ ] else [
+ 'sum by (job) (rate(prometheus_target_scrapes_exceeded_body_size_limit_total[1m]))',
+ 'sum by (job) (rate(prometheus_target_scrapes_exceeded_sample_limit_total[1m]))',
+ 'sum by (job) (rate(prometheus_target_scrapes_sample_duplicate_timestamp_total[1m]))',
+ 'sum by (job) (rate(prometheus_target_scrapes_sample_out_of_bounds_total[1m]))',
+ 'sum by (job) (rate(prometheus_target_scrapes_sample_out_of_order_total[1m]))',
+ ], if showMultiCluster then [
'exceeded body size limit: {{cluster}} {{job}} {{instance}}',
'exceeded sample limit: {{cluster}} {{job}} {{instance}}',
'duplicate timestamp: {{cluster}} {{job}} {{instance}}',
'out of bounds: {{cluster}} {{job}} {{instance}}',
'out of order: {{cluster}} {{job}} {{instance}}',
+ ] else [
+ 'exceeded body size limit: {{job}}',
+ 'exceeded sample limit: {{job}}',
+ 'duplicate timestamp: {{job}}',
+ 'out of bounds: {{job}}',
+ 'out of order: {{job}}',
]) +
g.stack
)
.addPanel(
g.panel('Appended Samples') +
- g.queryPanel('rate(prometheus_tsdb_head_samples_appended_total{cluster=~"$cluster", job=~"$job",instance=~"$instance"}[5m])', '{{cluster}} {{job}} {{instance}}') +
+ g.queryPanel(if showMultiCluster then 'rate(prometheus_tsdb_head_samples_appended_total{cluster=~"$cluster", job=~"$job",instance=~"$instance"}[5m])'
+ else 'rate(prometheus_tsdb_head_samples_appended_total{job=~"$job",instance=~"$instance"}[5m])',
+ if showMultiCluster then '{{cluster}} {{job}} {{instance}}'
+ else '{{job}} {{instance}}') +
g.stack
)
)
@@ -80,12 +115,18 @@ local template = grafana.template;
g.row('Storage')
.addPanel(
g.panel('Head Series') +
- g.queryPanel('prometheus_tsdb_head_series{cluster=~"$cluster",job=~"$job",instance=~"$instance"}', '{{cluster}} {{job}} {{instance}} head series') +
+ g.queryPanel(if showMultiCluster then 'prometheus_tsdb_head_series{cluster=~"$cluster",job=~"$job",instance=~"$instance"}'
+ else 'prometheus_tsdb_head_series{job=~"$job",instance=~"$instance"}',
+ if showMultiCluster then '{{cluster}} {{job}} {{instance}} head series'
+ else '{{job}} {{instance}} head series') +
g.stack
)
.addPanel(
g.panel('Head Chunks') +
- g.queryPanel('prometheus_tsdb_head_chunks{cluster=~"$cluster",job=~"$job",instance=~"$instance"}', '{{cluster}} {{job}} {{instance}} head chunks') +
+ g.queryPanel(if showMultiCluster then 'prometheus_tsdb_head_chunks{cluster=~"$cluster",job=~"$job",instance=~"$instance"}'
+ else 'prometheus_tsdb_head_chunks{job=~"$job",instance=~"$instance"}',
+ if showMultiCluster then '{{cluster}} {{job}} {{instance}} head chunks'
+ else '{{job}} {{instance}} head chunks') +
g.stack
)
)
@@ -93,12 +134,18 @@ local template = grafana.template;
g.row('Query')
.addPanel(
g.panel('Query Rate') +
- g.queryPanel('rate(prometheus_engine_query_duration_seconds_count{cluster=~"$cluster",job=~"$job",instance=~"$instance",slice="inner_eval"}[5m])', '{{cluster}} {{job}} {{instance}}') +
+ g.queryPanel(if showMultiCluster then 'rate(prometheus_engine_query_duration_seconds_count{cluster=~"$cluster",job=~"$job",instance=~"$instance",slice="inner_eval"}[5m])'
+ else 'rate(prometheus_engine_query_duration_seconds_count{job=~"$job",instance=~"$instance",slice="inner_eval"}[5m])',
+ if showMultiCluster then '{{cluster}} {{job}} {{instance}}'
+ else '{{job}} {{instance}}') +
g.stack,
)
.addPanel(
g.panel('Stage Duration') +
- g.queryPanel('max by (slice) (prometheus_engine_query_duration_seconds{quantile="0.9",cluster=~"$cluster", job=~"$job",instance=~"$instance"}) * 1e3', '{{slice}}') +
+ g.queryPanel(if showMultiCluster then 'max by (slice) (prometheus_engine_query_duration_seconds{quantile="0.9",cluster=~"$cluster", job=~"$job",instance=~"$instance"}) * 1e3'
+ else 'max by (slice) (prometheus_engine_query_duration_seconds{quantile="0.9",job=~"$job",instance=~"$instance"}) * 1e3',
+ if showMultiCluster then '{{slice}}'
+ else '{{slice}}') +
{ yaxes: g.yaxes('ms') } +
g.stack,
)
diff --git a/model/labels/regexp.go b/model/labels/regexp.go
index 79e340984a..b484e27168 100644
--- a/model/labels/regexp.go
+++ b/model/labels/regexp.go
@@ -828,7 +828,12 @@ type zeroOrOneCharacterStringMatcher struct {
}
func (m *zeroOrOneCharacterStringMatcher) Matches(s string) bool {
- if moreThanOneRune(s) {
+ // If there's more than one rune in the string, then it can't match.
+ if r, size := utf8.DecodeRuneInString(s); r == utf8.RuneError {
+ // Size is 0 for empty strings, 1 for invalid rune.
+ // Empty string matches, invalid rune matches if there isn't anything else.
+ return size == len(s)
+ } else if size < len(s) {
return false
}
@@ -840,27 +845,6 @@ func (m *zeroOrOneCharacterStringMatcher) Matches(s string) bool {
return s[0] != '\n'
}
-// moreThanOneRune returns true if there are more than one runes in the string.
-// It doesn't check whether the string is valid UTF-8.
-// The return value should be always equal to utf8.RuneCountInString(s) > 1,
-// but the function is optimized for the common case where the string prefix is ASCII.
-func moreThanOneRune(s string) bool {
- // If len(s) is exactly one or zero, there can't be more than one rune.
- // Exit through this path quickly.
- if len(s) <= 1 {
- return false
- }
-
- // There's one or more bytes:
- // If first byte is ASCII then there are multiple runes if there are more bytes after that.
- if s[0] < utf8.RuneSelf {
- return len(s) > 1
- }
-
- // Less common case: first is a multibyte rune.
- return utf8.RuneCountInString(s) > 1
-}
-
// trueMatcher is a stringMatcher which matches any string (always returns true).
type trueMatcher struct{}
diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go
index 47d3eeb4a2..1db90a473d 100644
--- a/model/labels/regexp_test.go
+++ b/model/labels/regexp_test.go
@@ -19,6 +19,7 @@ import (
"strings"
"testing"
"time"
+ "unicode/utf8"
"github.com/grafana/regexp"
"github.com/grafana/regexp/syntax"
@@ -36,6 +37,7 @@ var (
".*foo",
"^.*foo$",
"^.+foo$",
+ ".?",
".*",
".+",
"foo.+",
@@ -88,6 +90,12 @@ var (
// Values matching / not matching the test regexps on long alternations.
"zQPbMkNO", "zQPbMkNo", "jyyfj00j0061", "jyyfj00j006", "jyyfj00j00612", "NNSPdvMi", "NNSPdvMiXXX", "NNSPdvMixxx", "nnSPdvMi", "nnSPdvMiXXX",
+
+ // Invalid utf8
+ "\xfefoo",
+ "foo\xfe",
+ "\xfd",
+ "\xff\xff",
}
)
@@ -926,19 +934,91 @@ func BenchmarkOptimizeEqualStringMatchers(b *testing.B) {
}
func TestZeroOrOneCharacterStringMatcher(t *testing.T) {
- matcher := &zeroOrOneCharacterStringMatcher{matchNL: true}
- require.True(t, matcher.Matches(""))
- require.True(t, matcher.Matches("x"))
- require.True(t, matcher.Matches("\n"))
- require.False(t, matcher.Matches("xx"))
- require.False(t, matcher.Matches("\n\n"))
+ t.Run("match newline", func(t *testing.T) {
+ matcher := &zeroOrOneCharacterStringMatcher{matchNL: true}
+ require.True(t, matcher.Matches(""))
+ require.True(t, matcher.Matches("x"))
+ require.True(t, matcher.Matches("\n"))
+ require.False(t, matcher.Matches("xx"))
+ require.False(t, matcher.Matches("\n\n"))
+ })
- matcher = &zeroOrOneCharacterStringMatcher{matchNL: false}
- require.True(t, matcher.Matches(""))
- require.True(t, matcher.Matches("x"))
- require.False(t, matcher.Matches("\n"))
- require.False(t, matcher.Matches("xx"))
- require.False(t, matcher.Matches("\n\n"))
+ t.Run("do not match newline", func(t *testing.T) {
+ matcher := &zeroOrOneCharacterStringMatcher{matchNL: false}
+ require.True(t, matcher.Matches(""))
+ require.True(t, matcher.Matches("x"))
+ require.False(t, matcher.Matches("\n"))
+ require.False(t, matcher.Matches("xx"))
+ require.False(t, matcher.Matches("\n\n"))
+ })
+
+ t.Run("unicode", func(t *testing.T) {
+ // Just for documentation purposes, emoji1 is 1 rune, emoji2 is 2 runes.
+ // Having this in mind, will make future readers fixing tests easier.
+ emoji1 := "😀"
+ emoji2 := "❤️"
+ require.Equal(t, 1, utf8.RuneCountInString(emoji1))
+ require.Equal(t, 2, utf8.RuneCountInString(emoji2))
+
+ matcher := &zeroOrOneCharacterStringMatcher{matchNL: true}
+ require.True(t, matcher.Matches(emoji1))
+ require.False(t, matcher.Matches(emoji2))
+ require.False(t, matcher.Matches(emoji1+emoji1))
+ require.False(t, matcher.Matches("x"+emoji1))
+ require.False(t, matcher.Matches(emoji1+"x"))
+ require.False(t, matcher.Matches(emoji1+emoji2))
+ })
+
+ t.Run("invalid unicode", func(t *testing.T) {
+ // Just for reference, we also compare to what `^.?$` regular expression matches.
+ re := regexp.MustCompile("^.?$")
+ matcher := &zeroOrOneCharacterStringMatcher{matchNL: true}
+
+ requireMatches := func(s string, expected bool) {
+ t.Helper()
+ require.Equal(t, expected, matcher.Matches(s))
+ require.Equal(t, re.MatchString(s), matcher.Matches(s))
+ }
+
+ requireMatches("\xff", true)
+ requireMatches("x\xff", false)
+ requireMatches("\xffx", false)
+ requireMatches("\xff\xfe", false)
+ })
+}
+
+func BenchmarkZeroOrOneCharacterStringMatcher(b *testing.B) {
+ type benchCase struct {
+ str string
+ matches bool
+ }
+
+ emoji1 := "😀"
+ emoji2 := "❤️"
+ cases := []benchCase{
+ {"", true},
+ {"x", true},
+ {"\n", true},
+ {"xx", false},
+ {"\n\n", false},
+ {emoji1, true},
+ {emoji2, false},
+ {emoji1 + emoji1, false},
+ {strings.Repeat("x", 100), false},
+ {strings.Repeat(emoji1, 100), false},
+ {strings.Repeat(emoji2, 100), false},
+ }
+
+ matcher := &zeroOrOneCharacterStringMatcher{matchNL: true}
+ b.ResetTimer()
+
+ for n := 0; n < b.N; n++ {
+ c := cases[n%len(cases)]
+ got := matcher.Matches(c.str)
+ if got != c.matches {
+ b.Fatalf("unexpected result for %q: got %t, want %t", c.str, got, c.matches)
+ }
+ }
}
func TestLiteralPrefixStringMatcher(t *testing.T) {
diff --git a/promql/engine_test.go b/promql/engine_test.go
index f431ab41e8..b7435d4738 100644
--- a/promql/engine_test.go
+++ b/promql/engine_test.go
@@ -36,8 +36,6 @@ import (
"github.com/prometheus/prometheus/promql/parser/posrange"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/storage"
- "github.com/prometheus/prometheus/tsdb/tsdbutil"
- "github.com/prometheus/prometheus/util/almost"
"github.com/prometheus/prometheus/util/annotations"
"github.com/prometheus/prometheus/util/stats"
"github.com/prometheus/prometheus/util/teststorage"
@@ -3224,1076 +3222,6 @@ func TestRangeQuery(t *testing.T) {
}
}
-func TestNativeHistogramRate(t *testing.T) {
- // TODO(beorn7): Integrate histograms into the PromQL testing framework
- // and write more tests there.
- engine := newTestEngine()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
-
- seriesName := "sparse_histogram_series"
- lbls := labels.FromStrings("__name__", seriesName)
-
- app := storage.Appender(context.Background())
- for i, h := range tsdbutil.GenerateTestHistograms(100) {
- _, err := app.AppendHistogram(0, lbls, int64(i)*int64(15*time.Second/time.Millisecond), h, nil)
- require.NoError(t, err)
- }
- require.NoError(t, app.Commit())
-
- queryString := fmt.Sprintf("rate(%s[45s])", seriesName)
- t.Run("instant_query", func(t *testing.T) {
- qry, err := engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(int64(5*time.Minute/time.Millisecond)))
- require.NoError(t, err)
- res := qry.Exec(context.Background())
- require.NoError(t, res.Err)
- vector, err := res.Vector()
- require.NoError(t, err)
- require.Len(t, vector, 1)
- actualHistogram := vector[0].H
- expectedHistogram := &histogram.FloatHistogram{
- CounterResetHint: histogram.GaugeType,
- Schema: 1,
- ZeroThreshold: 0.001,
- ZeroCount: 1. / 15.,
- Count: 9. / 15.,
- Sum: 1.2266666666666663,
- PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- PositiveBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- NegativeSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- NegativeBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- }
- require.Equal(t, expectedHistogram, actualHistogram)
- })
-
- t.Run("range_query", func(t *testing.T) {
- step := 30 * time.Second
- start := timestamp.Time(int64(5 * time.Minute / time.Millisecond))
- end := start.Add(step)
- qry, err := engine.NewRangeQuery(context.Background(), storage, nil, queryString, start, end, step)
- require.NoError(t, err)
- res := qry.Exec(context.Background())
- require.NoError(t, res.Err)
- matrix, err := res.Matrix()
- require.NoError(t, err)
- require.Len(t, matrix, 1)
- require.Len(t, matrix[0].Histograms, 2)
- actualHistograms := matrix[0].Histograms
- expectedHistograms := []promql.HPoint{{
- T: 300000,
- H: &histogram.FloatHistogram{
- CounterResetHint: histogram.GaugeType,
- Schema: 1,
- ZeroThreshold: 0.001,
- ZeroCount: 1. / 15.,
- Count: 9. / 15.,
- Sum: 1.2266666666666663,
- PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- PositiveBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- NegativeSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- NegativeBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- },
- }, {
- T: 330000,
- H: &histogram.FloatHistogram{
- CounterResetHint: histogram.GaugeType,
- Schema: 1,
- ZeroThreshold: 0.001,
- ZeroCount: 1. / 15.,
- Count: 9. / 15.,
- Sum: 1.2266666666666663,
- PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- PositiveBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- NegativeSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- NegativeBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- },
- }}
- require.Equal(t, expectedHistograms, actualHistograms)
- })
-}
-
-func TestNativeFloatHistogramRate(t *testing.T) {
- // TODO(beorn7): Integrate histograms into the PromQL testing framework
- // and write more tests there.
- engine := newTestEngine()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
-
- seriesName := "sparse_histogram_series"
- lbls := labels.FromStrings("__name__", seriesName)
-
- app := storage.Appender(context.Background())
- for i, fh := range tsdbutil.GenerateTestFloatHistograms(100) {
- _, err := app.AppendHistogram(0, lbls, int64(i)*int64(15*time.Second/time.Millisecond), nil, fh)
- require.NoError(t, err)
- }
- require.NoError(t, app.Commit())
-
- queryString := fmt.Sprintf("rate(%s[1m])", seriesName)
- qry, err := engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(int64(5*time.Minute/time.Millisecond)))
- require.NoError(t, err)
- res := qry.Exec(context.Background())
- require.NoError(t, res.Err)
- vector, err := res.Vector()
- require.NoError(t, err)
- require.Len(t, vector, 1)
- actualHistogram := vector[0].H
- expectedHistogram := &histogram.FloatHistogram{
- CounterResetHint: histogram.GaugeType,
- Schema: 1,
- ZeroThreshold: 0.001,
- ZeroCount: 1. / 15.,
- Count: 9. / 15.,
- Sum: 1.226666666666667,
- PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- PositiveBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- NegativeSpans: []histogram.Span{{Offset: 0, Length: 2}, {Offset: 1, Length: 2}},
- NegativeBuckets: []float64{1. / 15., 1. / 15., 1. / 15., 1. / 15.},
- }
- require.Equal(t, expectedHistogram, actualHistogram)
-}
-
-func TestNativeHistogram_HistogramCountAndSum(t *testing.T) {
- // TODO(codesome): Integrate histograms into the PromQL testing framework
- // and write more tests there.
- h := &histogram.Histogram{
- Count: 24,
- ZeroCount: 4,
- ZeroThreshold: 0.001,
- Sum: 100,
- Schema: 0,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- PositiveBuckets: []int64{2, 1, -2, 3},
- NegativeSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- NegativeBuckets: []int64{2, 1, -2, 3},
- }
- for _, floatHisto := range []bool{true, false} {
- t.Run(fmt.Sprintf("floatHistogram=%t", floatHisto), func(t *testing.T) {
- engine := newTestEngine()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
-
- seriesName := "sparse_histogram_series"
- lbls := labels.FromStrings("__name__", seriesName)
-
- ts := int64(10 * time.Minute / time.Millisecond)
- app := storage.Appender(context.Background())
- var err error
- if floatHisto {
- _, err = app.AppendHistogram(0, lbls, ts, nil, h.ToFloat(nil))
- } else {
- _, err = app.AppendHistogram(0, lbls, ts, h, nil)
- }
- require.NoError(t, err)
- require.NoError(t, app.Commit())
-
- queryString := fmt.Sprintf("histogram_count(%s)", seriesName)
- qry, err := engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(ts))
- require.NoError(t, err)
-
- res := qry.Exec(context.Background())
- require.NoError(t, res.Err)
-
- vector, err := res.Vector()
- require.NoError(t, err)
-
- require.Len(t, vector, 1)
- require.Nil(t, vector[0].H)
- if floatHisto {
- require.Equal(t, h.ToFloat(nil).Count, vector[0].F)
- } else {
- require.Equal(t, float64(h.Count), vector[0].F)
- }
-
- queryString = fmt.Sprintf("histogram_sum(%s)", seriesName)
- qry, err = engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(ts))
- require.NoError(t, err)
-
- res = qry.Exec(context.Background())
- require.NoError(t, res.Err)
-
- vector, err = res.Vector()
- require.NoError(t, err)
-
- require.Len(t, vector, 1)
- require.Nil(t, vector[0].H)
- if floatHisto {
- require.Equal(t, h.ToFloat(nil).Sum, vector[0].F)
- } else {
- require.Equal(t, h.Sum, vector[0].F)
- }
- })
- }
-}
-
-func TestNativeHistogram_HistogramStdDevVar(t *testing.T) {
- // TODO(codesome): Integrate histograms into the PromQL testing framework
- // and write more tests there.
- testCases := []struct {
- name string
- h *histogram.Histogram
- stdVar float64
- }{
- {
- name: "1, 2, 3, 4 low-res",
- h: &histogram.Histogram{
- Count: 4,
- Sum: 10,
- Schema: 2,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 1},
- {Offset: 3, Length: 1},
- {Offset: 2, Length: 2},
- },
- PositiveBuckets: []int64{1, 0, 0, 0},
- },
- stdVar: 1.163807968526718, // actual variance: 1.25
- },
- {
- name: "1, 2, 3, 4 hi-res",
- h: &histogram.Histogram{
- Count: 4,
- Sum: 10,
- Schema: 8,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 1},
- {Offset: 255, Length: 1},
- {Offset: 149, Length: 1},
- {Offset: 105, Length: 1},
- },
- PositiveBuckets: []int64{1, 0, 0, 0},
- },
- stdVar: 1.2471347737158793, // actual variance: 1.25
- },
- {
- name: "-50, -8, 0, 3, 8, 9, 100",
- h: &histogram.Histogram{
- Count: 7,
- ZeroCount: 1,
- Sum: 62,
- Schema: 3,
- PositiveSpans: []histogram.Span{
- {Offset: 13, Length: 1},
- {Offset: 10, Length: 1},
- {Offset: 1, Length: 1},
- {Offset: 27, Length: 1},
- },
- PositiveBuckets: []int64{1, 0, 0, 0},
- NegativeSpans: []histogram.Span{
- {Offset: 24, Length: 1},
- {Offset: 21, Length: 1},
- },
- NegativeBuckets: []int64{1, 0},
- },
- stdVar: 1844.4651144196398, // actual variance: 1738.4082
- },
- {
- name: "-100000, -10000, -1000, -888, -888, -100, -50, -9, -8, -3",
- h: &histogram.Histogram{
- Count: 10,
- ZeroCount: 0,
- Sum: -112946,
- Schema: 0,
- NegativeSpans: []histogram.Span{
- {Offset: 2, Length: 3},
- {Offset: 1, Length: 2},
- {Offset: 2, Length: 1},
- {Offset: 3, Length: 1},
- {Offset: 2, Length: 1},
- },
- NegativeBuckets: []int64{1, 0, 0, 0, 0, 2, -2, 0},
- },
- stdVar: 759352122.1939945, // actual variance: 882690990
- },
- {
- name: "-10 x10",
- h: &histogram.Histogram{
- Count: 10,
- ZeroCount: 0,
- Sum: -100,
- Schema: 0,
- NegativeSpans: []histogram.Span{
- {Offset: 4, Length: 1},
- },
- NegativeBuckets: []int64{10},
- },
- stdVar: 1.725830020304794, // actual variance: 0
- },
- {
- name: "-50, -8, 0, 3, 8, 9, 100, NaN",
- h: &histogram.Histogram{
- Count: 8,
- ZeroCount: 1,
- Sum: math.NaN(),
- Schema: 3,
- PositiveSpans: []histogram.Span{
- {Offset: 13, Length: 1},
- {Offset: 10, Length: 1},
- {Offset: 1, Length: 1},
- {Offset: 27, Length: 1},
- },
- PositiveBuckets: []int64{1, 0, 0, 0},
- NegativeSpans: []histogram.Span{
- {Offset: 24, Length: 1},
- {Offset: 21, Length: 1},
- },
- NegativeBuckets: []int64{1, 0},
- },
- stdVar: math.NaN(),
- },
- {
- name: "-50, -8, 0, 3, 8, 9, 100, +Inf",
- h: &histogram.Histogram{
- Count: 7,
- ZeroCount: 1,
- Sum: math.Inf(1),
- Schema: 3,
- PositiveSpans: []histogram.Span{
- {Offset: 13, Length: 1},
- {Offset: 10, Length: 1},
- {Offset: 1, Length: 1},
- {Offset: 27, Length: 1},
- },
- PositiveBuckets: []int64{1, 0, 0, 0},
- NegativeSpans: []histogram.Span{
- {Offset: 24, Length: 1},
- {Offset: 21, Length: 1},
- },
- NegativeBuckets: []int64{1, 0},
- },
- stdVar: math.NaN(),
- },
- }
- for _, tc := range testCases {
- for _, floatHisto := range []bool{true, false} {
- t.Run(fmt.Sprintf("%s floatHistogram=%t", tc.name, floatHisto), func(t *testing.T) {
- engine := newTestEngine()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
-
- seriesName := "sparse_histogram_series"
- lbls := labels.FromStrings("__name__", seriesName)
-
- ts := int64(10 * time.Minute / time.Millisecond)
- app := storage.Appender(context.Background())
- var err error
- if floatHisto {
- _, err = app.AppendHistogram(0, lbls, ts, nil, tc.h.ToFloat(nil))
- } else {
- _, err = app.AppendHistogram(0, lbls, ts, tc.h, nil)
- }
- require.NoError(t, err)
- require.NoError(t, app.Commit())
-
- queryString := fmt.Sprintf("histogram_stdvar(%s)", seriesName)
- qry, err := engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(ts))
- require.NoError(t, err)
-
- res := qry.Exec(context.Background())
- require.NoError(t, res.Err)
-
- vector, err := res.Vector()
- require.NoError(t, err)
-
- require.Len(t, vector, 1)
- require.Nil(t, vector[0].H)
- require.InEpsilon(t, tc.stdVar, vector[0].F, 1e-12)
-
- queryString = fmt.Sprintf("histogram_stddev(%s)", seriesName)
- qry, err = engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(ts))
- require.NoError(t, err)
-
- res = qry.Exec(context.Background())
- require.NoError(t, res.Err)
-
- vector, err = res.Vector()
- require.NoError(t, err)
-
- require.Len(t, vector, 1)
- require.Nil(t, vector[0].H)
- require.InEpsilon(t, math.Sqrt(tc.stdVar), vector[0].F, 1e-12)
- })
- }
- }
-}
-
-func TestNativeHistogram_HistogramQuantile(t *testing.T) {
- // TODO(codesome): Integrate histograms into the PromQL testing framework
- // and write more tests there.
- type subCase struct {
- quantile string
- value float64
- }
-
- cases := []struct {
- text string
- // Histogram to test.
- h *histogram.Histogram
- // Different quantiles to test for this histogram.
- subCases []subCase
- }{
- {
- text: "all positive buckets with zero bucket",
- h: &histogram.Histogram{
- Count: 12,
- ZeroCount: 2,
- ZeroThreshold: 0.001,
- Sum: 100, // Does not matter.
- Schema: 0,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- PositiveBuckets: []int64{2, 1, -2, 3},
- },
- subCases: []subCase{
- {
- quantile: "1.0001",
- value: math.Inf(1),
- },
- {
- quantile: "1",
- value: 16,
- },
- {
- quantile: "0.99",
- value: 15.759999999999998,
- },
- {
- quantile: "0.9",
- value: 13.600000000000001,
- },
- {
- quantile: "0.6",
- value: 4.799999999999997,
- },
- {
- quantile: "0.5",
- value: 1.6666666666666665,
- },
- { // Zero bucket.
- quantile: "0.1",
- value: 0.0006000000000000001,
- },
- {
- quantile: "0",
- value: 0,
- },
- {
- quantile: "-1",
- value: math.Inf(-1),
- },
- },
- },
- {
- text: "all negative buckets with zero bucket",
- h: &histogram.Histogram{
- Count: 12,
- ZeroCount: 2,
- ZeroThreshold: 0.001,
- Sum: 100, // Does not matter.
- Schema: 0,
- NegativeSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- NegativeBuckets: []int64{2, 1, -2, 3},
- },
- subCases: []subCase{
- {
- quantile: "1.0001",
- value: math.Inf(1),
- },
- { // Zero bucket.
- quantile: "1",
- value: 0,
- },
- { // Zero bucket.
- quantile: "0.99",
- value: -6.000000000000048e-05,
- },
- { // Zero bucket.
- quantile: "0.9",
- value: -0.0005999999999999996,
- },
- {
- quantile: "0.5",
- value: -1.6666666666666667,
- },
- {
- quantile: "0.1",
- value: -13.6,
- },
- {
- quantile: "0",
- value: -16,
- },
- {
- quantile: "-1",
- value: math.Inf(-1),
- },
- },
- },
- {
- text: "both positive and negative buckets with zero bucket",
- h: &histogram.Histogram{
- Count: 24,
- ZeroCount: 4,
- ZeroThreshold: 0.001,
- Sum: 100, // Does not matter.
- Schema: 0,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- PositiveBuckets: []int64{2, 1, -2, 3},
- NegativeSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- NegativeBuckets: []int64{2, 1, -2, 3},
- },
- subCases: []subCase{
- {
- quantile: "1.0001",
- value: math.Inf(1),
- },
- {
- quantile: "1",
- value: 16,
- },
- {
- quantile: "0.99",
- value: 15.519999999999996,
- },
- {
- quantile: "0.9",
- value: 11.200000000000003,
- },
- {
- quantile: "0.7",
- value: 1.2666666666666657,
- },
- { // Zero bucket.
- quantile: "0.55",
- value: 0.0006000000000000005,
- },
- { // Zero bucket.
- quantile: "0.5",
- value: 0,
- },
- { // Zero bucket.
- quantile: "0.45",
- value: -0.0005999999999999996,
- },
- {
- quantile: "0.3",
- value: -1.266666666666667,
- },
- {
- quantile: "0.1",
- value: -11.2,
- },
- {
- quantile: "0.01",
- value: -15.52,
- },
- {
- quantile: "0",
- value: -16,
- },
- {
- quantile: "-1",
- value: math.Inf(-1),
- },
- },
- },
- }
-
- engine := newTestEngine()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
- idx := int64(0)
- for _, floatHisto := range []bool{true, false} {
- for _, c := range cases {
- t.Run(fmt.Sprintf("%s floatHistogram=%t", c.text, floatHisto), func(t *testing.T) {
- seriesName := "sparse_histogram_series"
- lbls := labels.FromStrings("__name__", seriesName)
- ts := idx * int64(10*time.Minute/time.Millisecond)
- app := storage.Appender(context.Background())
- var err error
- if floatHisto {
- _, err = app.AppendHistogram(0, lbls, ts, nil, c.h.ToFloat(nil))
- } else {
- _, err = app.AppendHistogram(0, lbls, ts, c.h, nil)
- }
- require.NoError(t, err)
- require.NoError(t, app.Commit())
-
- for j, sc := range c.subCases {
- t.Run(fmt.Sprintf("%d %s", j, sc.quantile), func(t *testing.T) {
- queryString := fmt.Sprintf("histogram_quantile(%s, %s)", sc.quantile, seriesName)
- qry, err := engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(ts))
- require.NoError(t, err)
-
- res := qry.Exec(context.Background())
- require.NoError(t, res.Err)
-
- vector, err := res.Vector()
- require.NoError(t, err)
-
- require.Len(t, vector, 1)
- require.Nil(t, vector[0].H)
- require.True(t, almost.Equal(sc.value, vector[0].F, defaultEpsilon))
- })
- }
- idx++
- })
- }
- }
-}
-
-func TestNativeHistogram_HistogramFraction(t *testing.T) {
- // TODO(codesome): Integrate histograms into the PromQL testing framework
- // and write more tests there.
- type subCase struct {
- lower, upper string
- value float64
- }
-
- invariantCases := []subCase{
- {
- lower: "42",
- upper: "3.1415",
- value: 0,
- },
- {
- lower: "0",
- upper: "0",
- value: 0,
- },
- {
- lower: "0.000001",
- upper: "0.000001",
- value: 0,
- },
- {
- lower: "42",
- upper: "42",
- value: 0,
- },
- {
- lower: "-3.1",
- upper: "-3.1",
- value: 0,
- },
- {
- lower: "3.1415",
- upper: "NaN",
- value: math.NaN(),
- },
- {
- lower: "NaN",
- upper: "42",
- value: math.NaN(),
- },
- {
- lower: "NaN",
- upper: "NaN",
- value: math.NaN(),
- },
- {
- lower: "-Inf",
- upper: "+Inf",
- value: 1,
- },
- }
-
- cases := []struct {
- text string
- // Histogram to test.
- h *histogram.Histogram
- // Different ranges to test for this histogram.
- subCases []subCase
- }{
- {
- text: "empty histogram",
- h: &histogram.Histogram{},
- subCases: []subCase{
- {
- lower: "3.1415",
- upper: "42",
- value: math.NaN(),
- },
- },
- },
- {
- text: "all positive buckets with zero bucket",
- h: &histogram.Histogram{
- Count: 12,
- ZeroCount: 2,
- ZeroThreshold: 0.001,
- Sum: 100, // Does not matter.
- Schema: 0,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- PositiveBuckets: []int64{2, 1, -2, 3}, // Abs: 2, 3, 1, 4
- },
- subCases: append([]subCase{
- {
- lower: "0",
- upper: "+Inf",
- value: 1,
- },
- {
- lower: "-Inf",
- upper: "0",
- value: 0,
- },
- {
- lower: "-0.001",
- upper: "0",
- value: 0,
- },
- {
- lower: "0",
- upper: "0.001",
- value: 2. / 12.,
- },
- {
- lower: "0",
- upper: "0.0005",
- value: 1. / 12.,
- },
- {
- lower: "0.001",
- upper: "inf",
- value: 10. / 12.,
- },
- {
- lower: "-inf",
- upper: "-0.001",
- value: 0,
- },
- {
- lower: "1",
- upper: "2",
- value: 3. / 12.,
- },
- {
- lower: "1.5",
- upper: "2",
- value: 1.5 / 12.,
- },
- {
- lower: "1",
- upper: "8",
- value: 4. / 12.,
- },
- {
- lower: "1",
- upper: "6",
- value: 3.5 / 12.,
- },
- {
- lower: "1.5",
- upper: "6",
- value: 2. / 12.,
- },
- {
- lower: "-2",
- upper: "-1",
- value: 0,
- },
- {
- lower: "-2",
- upper: "-1.5",
- value: 0,
- },
- {
- lower: "-8",
- upper: "-1",
- value: 0,
- },
- {
- lower: "-6",
- upper: "-1",
- value: 0,
- },
- {
- lower: "-6",
- upper: "-1.5",
- value: 0,
- },
- }, invariantCases...),
- },
- {
- text: "all negative buckets with zero bucket",
- h: &histogram.Histogram{
- Count: 12,
- ZeroCount: 2,
- ZeroThreshold: 0.001,
- Sum: 100, // Does not matter.
- Schema: 0,
- NegativeSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- NegativeBuckets: []int64{2, 1, -2, 3},
- },
- subCases: append([]subCase{
- {
- lower: "0",
- upper: "+Inf",
- value: 0,
- },
- {
- lower: "-Inf",
- upper: "0",
- value: 1,
- },
- {
- lower: "-0.001",
- upper: "0",
- value: 2. / 12.,
- },
- {
- lower: "0",
- upper: "0.001",
- value: 0,
- },
- {
- lower: "-0.0005",
- upper: "0",
- value: 1. / 12.,
- },
- {
- lower: "0.001",
- upper: "inf",
- value: 0,
- },
- {
- lower: "-inf",
- upper: "-0.001",
- value: 10. / 12.,
- },
- {
- lower: "1",
- upper: "2",
- value: 0,
- },
- {
- lower: "1.5",
- upper: "2",
- value: 0,
- },
- {
- lower: "1",
- upper: "8",
- value: 0,
- },
- {
- lower: "1",
- upper: "6",
- value: 0,
- },
- {
- lower: "1.5",
- upper: "6",
- value: 0,
- },
- {
- lower: "-2",
- upper: "-1",
- value: 3. / 12.,
- },
- {
- lower: "-2",
- upper: "-1.5",
- value: 1.5 / 12.,
- },
- {
- lower: "-8",
- upper: "-1",
- value: 4. / 12.,
- },
- {
- lower: "-6",
- upper: "-1",
- value: 3.5 / 12.,
- },
- {
- lower: "-6",
- upper: "-1.5",
- value: 2. / 12.,
- },
- }, invariantCases...),
- },
- {
- text: "both positive and negative buckets with zero bucket",
- h: &histogram.Histogram{
- Count: 24,
- ZeroCount: 4,
- ZeroThreshold: 0.001,
- Sum: 100, // Does not matter.
- Schema: 0,
- PositiveSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- PositiveBuckets: []int64{2, 1, -2, 3},
- NegativeSpans: []histogram.Span{
- {Offset: 0, Length: 2},
- {Offset: 1, Length: 2},
- },
- NegativeBuckets: []int64{2, 1, -2, 3},
- },
- subCases: append([]subCase{
- {
- lower: "0",
- upper: "+Inf",
- value: 0.5,
- },
- {
- lower: "-Inf",
- upper: "0",
- value: 0.5,
- },
- {
- lower: "-0.001",
- upper: "0",
- value: 2. / 24,
- },
- {
- lower: "0",
- upper: "0.001",
- value: 2. / 24.,
- },
- {
- lower: "-0.0005",
- upper: "0.0005",
- value: 2. / 24.,
- },
- {
- lower: "0.001",
- upper: "inf",
- value: 10. / 24.,
- },
- {
- lower: "-inf",
- upper: "-0.001",
- value: 10. / 24.,
- },
- {
- lower: "1",
- upper: "2",
- value: 3. / 24.,
- },
- {
- lower: "1.5",
- upper: "2",
- value: 1.5 / 24.,
- },
- {
- lower: "1",
- upper: "8",
- value: 4. / 24.,
- },
- {
- lower: "1",
- upper: "6",
- value: 3.5 / 24.,
- },
- {
- lower: "1.5",
- upper: "6",
- value: 2. / 24.,
- },
- {
- lower: "-2",
- upper: "-1",
- value: 3. / 24.,
- },
- {
- lower: "-2",
- upper: "-1.5",
- value: 1.5 / 24.,
- },
- {
- lower: "-8",
- upper: "-1",
- value: 4. / 24.,
- },
- {
- lower: "-6",
- upper: "-1",
- value: 3.5 / 24.,
- },
- {
- lower: "-6",
- upper: "-1.5",
- value: 2. / 24.,
- },
- }, invariantCases...),
- },
- }
- idx := int64(0)
- for _, floatHisto := range []bool{true, false} {
- for _, c := range cases {
- t.Run(fmt.Sprintf("%s floatHistogram=%t", c.text, floatHisto), func(t *testing.T) {
- engine := newTestEngine()
- storage := teststorage.New(t)
- t.Cleanup(func() { storage.Close() })
-
- seriesName := "sparse_histogram_series"
- lbls := labels.FromStrings("__name__", seriesName)
-
- ts := idx * int64(10*time.Minute/time.Millisecond)
- app := storage.Appender(context.Background())
- var err error
- if floatHisto {
- _, err = app.AppendHistogram(0, lbls, ts, nil, c.h.ToFloat(nil))
- } else {
- _, err = app.AppendHistogram(0, lbls, ts, c.h, nil)
- }
- require.NoError(t, err)
- require.NoError(t, app.Commit())
-
- for j, sc := range c.subCases {
- t.Run(fmt.Sprintf("%d %s %s", j, sc.lower, sc.upper), func(t *testing.T) {
- queryString := fmt.Sprintf("histogram_fraction(%s, %s, %s)", sc.lower, sc.upper, seriesName)
- qry, err := engine.NewInstantQuery(context.Background(), storage, nil, queryString, timestamp.Time(ts))
- require.NoError(t, err)
-
- res := qry.Exec(context.Background())
- require.NoError(t, res.Err)
-
- vector, err := res.Vector()
- require.NoError(t, err)
-
- require.Len(t, vector, 1)
- require.Nil(t, vector[0].H)
- if math.IsNaN(sc.value) {
- require.True(t, math.IsNaN(vector[0].F))
- return
- }
- require.Equal(t, sc.value, vector[0].F)
- })
- }
- idx++
- })
- }
- }
-}
-
func TestNativeHistogram_Sum_Count_Add_AvgOperator(t *testing.T) {
// TODO(codesome): Integrate histograms into the PromQL testing framework
// and write more tests there.
diff --git a/promql/promqltest/testdata/native_histograms.test b/promql/promqltest/testdata/native_histograms.test
index 1da68a385f..37818e4f88 100644
--- a/promql/promqltest/testdata/native_histograms.test
+++ b/promql/promqltest/testdata/native_histograms.test
@@ -269,3 +269,448 @@ eval instant at 50m histogram_sum(sum(incr_sum_histogram))
eval instant at 50m histogram_sum(sum(last_over_time(incr_sum_histogram[5m])))
{} 30
+
+# Apply rate function to histogram.
+load 15s
+ histogram_rate {{schema:1 count:12 sum:18.4 z_bucket:2 z_bucket_w:0.001 buckets:[1 2 0 1 1] n_buckets:[1 2 0 1 1]}}+{{schema:1 count:9 sum:18.4 z_bucket:1 z_bucket_w:0.001 buckets:[1 1 0 1 1] n_buckets:[1 1 0 1 1]}}x100
+
+eval instant at 5m rate(histogram_rate[45s])
+ {} {{schema:1 count:0.6 sum:1.2266666666666652 z_bucket:0.06666666666666667 z_bucket_w:0.001 buckets:[0.06666666666666667 0.06666666666666667 0 0.06666666666666667 0.06666666666666667] n_buckets:[0.06666666666666667 0.06666666666666667 0 0.06666666666666667 0.06666666666666667]}}
+
+eval range from 5m to 5m30s step 30s rate(histogram_rate[45s])
+ {} {{schema:1 count:0.6 sum:1.2266666666666652 z_bucket:0.06666666666666667 z_bucket_w:0.001 buckets:[0.06666666666666667 0.06666666666666667 0 0.06666666666666667 0.06666666666666667] n_buckets:[0.06666666666666667 0.06666666666666667 0 0.06666666666666667 0.06666666666666667]}}x1
+
+# Apply count and sum function to histogram.
+load 10m
+ histogram_count_sum_2 {{schema:0 count:24 sum:100 z_bucket:4 z_bucket_w:0.001 buckets:[2 3 0 1 4] n_buckets:[2 3 0 1 4]}}x1
+
+eval instant at 10m histogram_count(histogram_count_sum_2)
+ {} 24
+
+eval instant at 10m histogram_sum(histogram_count_sum_2)
+ {} 100
+
+# Apply stddev and stdvar function to histogram with {1, 2, 3, 4} (low res).
+load 10m
+ histogram_stddev_stdvar_1 {{schema:2 count:4 sum:10 buckets:[1 0 0 0 1 0 0 1 1]}}x1
+
+eval instant at 10m histogram_stddev(histogram_stddev_stdvar_1)
+ {} 1.0787993180043811
+
+eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_1)
+ {} 1.163807968526718
+
+# Apply stddev and stdvar function to histogram with {1, 1, 1, 1} (high res).
+load 10m
+ histogram_stddev_stdvar_2 {{schema:8 count:10 sum:10 buckets:[1 2 3 4]}}x1
+
+eval instant at 10m histogram_stddev(histogram_stddev_stdvar_2)
+ {} 0.0048960313898237465
+
+eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_2)
+ {} 2.3971123370139447e-05
+
+# Apply stddev and stdvar function to histogram with {-50, -8, 0, 3, 8, 9}.
+load 10m
+ histogram_stddev_stdvar_3 {{schema:3 count:7 sum:62 z_bucket:1 buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ] n_buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ]}}x1
+
+eval instant at 10m histogram_stddev(histogram_stddev_stdvar_3)
+ {} 42.947236400258
+
+eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_3)
+ {} 1844.4651144196398
+
+# Apply stddev and stdvar function to histogram with {-100000, -10000, -1000, -888, -888, -100, -50, -9, -8, -3}.
+load 10m
+ histogram_stddev_stdvar_4 {{schema:0 count:10 sum:-112946 z_bucket:0 n_buckets:[0 0 1 1 1 0 1 1 0 0 3 0 0 0 1 0 0 1]}}x1
+
+eval instant at 10m histogram_stddev(histogram_stddev_stdvar_4)
+ {} 27556.344499842
+
+eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_4)
+ {} 759352122.1939945
+
+# Apply stddev and stdvar function to histogram with {-10x10}.
+load 10m
+ histogram_stddev_stdvar_5 {{schema:0 count:10 sum:-100 z_bucket:0 n_buckets:[0 0 0 0 10]}}x1
+
+eval instant at 10m histogram_stddev(histogram_stddev_stdvar_5)
+ {} 1.3137084989848
+
+eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_5)
+ {} 1.725830020304794
+
+# Apply stddev and stdvar function to histogram with {-50, -8, 0, 3, 8, 9, NaN}.
+load 10m
+ histogram_stddev_stdvar_6 {{schema:3 count:7 sum:NaN z_bucket:1 buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ] n_buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ]}}x1
+
+eval instant at 10m histogram_stddev(histogram_stddev_stdvar_6)
+ {} NaN
+
+eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_6)
+ {} NaN
+
+# Apply stddev and stdvar function to histogram with {-50, -8, 0, 3, 8, 9, Inf}.
+load 10m
+ histogram_stddev_stdvar_7 {{schema:3 count:7 sum:Inf z_bucket:1 buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ] n_buckets:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ]}}x1
+
+eval instant at 10m histogram_stddev(histogram_stddev_stdvar_7)
+ {} NaN
+
+eval instant at 10m histogram_stdvar(histogram_stddev_stdvar_7)
+ {} NaN
+
+# Apply quantile function to histogram with all positive buckets with zero bucket.
+load 10m
+ histogram_quantile_1 {{schema:0 count:12 sum:100 z_bucket:2 z_bucket_w:0.001 buckets:[2 3 0 1 4]}}x1
+
+eval instant at 10m histogram_quantile(1.001, histogram_quantile_1)
+ {} Inf
+
+eval instant at 10m histogram_quantile(1, histogram_quantile_1)
+ {} 16
+
+eval instant at 10m histogram_quantile(0.99, histogram_quantile_1)
+ {} 15.759999999999998
+
+eval instant at 10m histogram_quantile(0.9, histogram_quantile_1)
+ {} 13.600000000000001
+
+eval instant at 10m histogram_quantile(0.6, histogram_quantile_1)
+ {} 4.799999999999997
+
+eval instant at 10m histogram_quantile(0.5, histogram_quantile_1)
+ {} 1.6666666666666665
+
+eval instant at 10m histogram_quantile(0.1, histogram_quantile_1)
+ {} 0.0006000000000000001
+
+eval instant at 10m histogram_quantile(0, histogram_quantile_1)
+ {} 0
+
+eval instant at 10m histogram_quantile(-1, histogram_quantile_1)
+ {} -Inf
+
+# Apply quantile function to histogram with all negative buckets with zero bucket.
+load 10m
+ histogram_quantile_2 {{schema:0 count:12 sum:100 z_bucket:2 z_bucket_w:0.001 n_buckets:[2 3 0 1 4]}}x1
+
+eval instant at 10m histogram_quantile(1.001, histogram_quantile_2)
+ {} Inf
+
+eval instant at 10m histogram_quantile(1, histogram_quantile_2)
+ {} 0
+
+eval instant at 10m histogram_quantile(0.99, histogram_quantile_2)
+ {} -6.000000000000048e-05
+
+eval instant at 10m histogram_quantile(0.9, histogram_quantile_2)
+ {} -0.0005999999999999996
+
+eval instant at 10m histogram_quantile(0.5, histogram_quantile_2)
+ {} -1.6666666666666667
+
+eval instant at 10m histogram_quantile(0.1, histogram_quantile_2)
+ {} -13.6
+
+eval instant at 10m histogram_quantile(0, histogram_quantile_2)
+ {} -16
+
+eval instant at 10m histogram_quantile(-1, histogram_quantile_2)
+ {} -Inf
+
+# Apply quantile function to histogram with both positive and negative buckets with zero bucket.
+load 10m
+ histogram_quantile_3 {{schema:0 count:24 sum:100 z_bucket:4 z_bucket_w:0.001 buckets:[2 3 0 1 4] n_buckets:[2 3 0 1 4]}}x1
+
+eval instant at 10m histogram_quantile(1.001, histogram_quantile_3)
+ {} Inf
+
+eval instant at 10m histogram_quantile(1, histogram_quantile_3)
+ {} 16
+
+eval instant at 10m histogram_quantile(0.99, histogram_quantile_3)
+ {} 15.519999999999996
+
+eval instant at 10m histogram_quantile(0.9, histogram_quantile_3)
+ {} 11.200000000000003
+
+eval instant at 10m histogram_quantile(0.7, histogram_quantile_3)
+ {} 1.2666666666666657
+
+eval instant at 10m histogram_quantile(0.55, histogram_quantile_3)
+ {} 0.0006000000000000005
+
+eval instant at 10m histogram_quantile(0.5, histogram_quantile_3)
+ {} 0
+
+eval instant at 10m histogram_quantile(0.45, histogram_quantile_3)
+ {} -0.0005999999999999996
+
+eval instant at 10m histogram_quantile(0.3, histogram_quantile_3)
+ {} -1.266666666666667
+
+eval instant at 10m histogram_quantile(0.1, histogram_quantile_3)
+ {} -11.2
+
+eval instant at 10m histogram_quantile(0.01, histogram_quantile_3)
+ {} -15.52
+
+eval instant at 10m histogram_quantile(0, histogram_quantile_3)
+ {} -16
+
+eval instant at 10m histogram_quantile(-1, histogram_quantile_3)
+ {} -Inf
+
+# Apply fraction function to empty histogram.
+load 10m
+ histogram_fraction_1 {{}}x1
+
+eval instant at 10m histogram_fraction(3.1415, 42, histogram_fraction_1)
+ {} NaN
+
+# Apply fraction function to histogram with positive and zero buckets.
+load 10m
+ histogram_fraction_2 {{schema:0 count:12 sum:100 z_bucket:2 z_bucket_w:0.001 buckets:[2 3 0 1 4]}}x1
+
+eval instant at 10m histogram_fraction(0, +Inf, histogram_fraction_2)
+ {} 1
+
+eval instant at 10m histogram_fraction(-Inf, 0, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(-0.001, 0, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(0, 0.001, histogram_fraction_2)
+ {} 0.16666666666666666
+
+eval instant at 10m histogram_fraction(0, 0.0005, histogram_fraction_2)
+ {} 0.08333333333333333
+
+eval instant at 10m histogram_fraction(0.001, inf, histogram_fraction_2)
+ {} 0.8333333333333334
+
+eval instant at 10m histogram_fraction(-inf, -0.001, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(1, 2, histogram_fraction_2)
+ {} 0.25
+
+eval instant at 10m histogram_fraction(1.5, 2, histogram_fraction_2)
+ {} 0.125
+
+eval instant at 10m histogram_fraction(1, 8, histogram_fraction_2)
+ {} 0.3333333333333333
+
+eval instant at 10m histogram_fraction(1, 6, histogram_fraction_2)
+ {} 0.2916666666666667
+
+eval instant at 10m histogram_fraction(1.5, 6, histogram_fraction_2)
+ {} 0.16666666666666666
+
+eval instant at 10m histogram_fraction(-2, -1, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(-2, -1.5, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(-8, -1, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(-6, -1, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(-6, -1.5, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(42, 3.1415, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(0, 0, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(0.000001, 0.000001, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(42, 42, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(-3.1, -3.1, histogram_fraction_2)
+ {} 0
+
+eval instant at 10m histogram_fraction(3.1415, NaN, histogram_fraction_2)
+ {} NaN
+
+eval instant at 10m histogram_fraction(NaN, 42, histogram_fraction_2)
+ {} NaN
+
+eval instant at 10m histogram_fraction(NaN, NaN, histogram_fraction_2)
+ {} NaN
+
+eval instant at 10m histogram_fraction(-Inf, +Inf, histogram_fraction_2)
+ {} 1
+
+# Apply fraction function to histogram with negative and zero buckets.
+load 10m
+ histogram_fraction_3 {{schema:0 count:12 sum:100 z_bucket:2 z_bucket_w:0.001 n_buckets:[2 3 0 1 4]}}x1
+
+eval instant at 10m histogram_fraction(0, +Inf, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(-Inf, 0, histogram_fraction_3)
+ {} 1
+
+eval instant at 10m histogram_fraction(-0.001, 0, histogram_fraction_3)
+ {} 0.16666666666666666
+
+eval instant at 10m histogram_fraction(0, 0.001, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(-0.0005, 0, histogram_fraction_3)
+ {} 0.08333333333333333
+
+eval instant at 10m histogram_fraction(0.001, inf, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(-inf, -0.001, histogram_fraction_3)
+ {} 0.8333333333333334
+
+eval instant at 10m histogram_fraction(1, 2, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(1.5, 2, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(1, 8, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(1, 6, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(1.5, 6, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(-2, -1, histogram_fraction_3)
+ {} 0.25
+
+eval instant at 10m histogram_fraction(-2, -1.5, histogram_fraction_3)
+ {} 0.125
+
+eval instant at 10m histogram_fraction(-8, -1, histogram_fraction_3)
+ {} 0.3333333333333333
+
+eval instant at 10m histogram_fraction(-6, -1, histogram_fraction_3)
+ {} 0.2916666666666667
+
+eval instant at 10m histogram_fraction(-6, -1.5, histogram_fraction_3)
+ {} 0.16666666666666666
+
+eval instant at 10m histogram_fraction(42, 3.1415, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(0, 0, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(0.000001, 0.000001, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(42, 42, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(-3.1, -3.1, histogram_fraction_3)
+ {} 0
+
+eval instant at 10m histogram_fraction(3.1415, NaN, histogram_fraction_3)
+ {} NaN
+
+eval instant at 10m histogram_fraction(NaN, 42, histogram_fraction_3)
+ {} NaN
+
+eval instant at 10m histogram_fraction(NaN, NaN, histogram_fraction_3)
+ {} NaN
+
+eval instant at 10m histogram_fraction(-Inf, +Inf, histogram_fraction_3)
+ {} 1
+
+# Apply fraction function to histogram with both positive, negative and zero buckets.
+load 10m
+ histogram_fraction_4 {{schema:0 count:24 sum:100 z_bucket:4 z_bucket_w:0.001 buckets:[2 3 0 1 4] n_buckets:[2 3 0 1 4]}}x1
+
+eval instant at 10m histogram_fraction(0, +Inf, histogram_fraction_4)
+ {} 0.5
+
+eval instant at 10m histogram_fraction(-Inf, 0, histogram_fraction_4)
+ {} 0.5
+
+eval instant at 10m histogram_fraction(-0.001, 0, histogram_fraction_4)
+ {} 0.08333333333333333
+
+eval instant at 10m histogram_fraction(0, 0.001, histogram_fraction_4)
+ {} 0.08333333333333333
+
+eval instant at 10m histogram_fraction(-0.0005, 0.0005, histogram_fraction_4)
+ {} 0.08333333333333333
+
+eval instant at 10m histogram_fraction(0.001, inf, histogram_fraction_4)
+ {} 0.4166666666666667
+
+eval instant at 10m histogram_fraction(-inf, -0.001, histogram_fraction_4)
+ {} 0.4166666666666667
+
+eval instant at 10m histogram_fraction(1, 2, histogram_fraction_4)
+ {} 0.125
+
+eval instant at 10m histogram_fraction(1.5, 2, histogram_fraction_4)
+ {} 0.0625
+
+eval instant at 10m histogram_fraction(1, 8, histogram_fraction_4)
+ {} 0.16666666666666666
+
+eval instant at 10m histogram_fraction(1, 6, histogram_fraction_4)
+ {} 0.14583333333333334
+
+eval instant at 10m histogram_fraction(1.5, 6, histogram_fraction_4)
+ {} 0.08333333333333333
+
+eval instant at 10m histogram_fraction(-2, -1, histogram_fraction_4)
+ {} 0.125
+
+eval instant at 10m histogram_fraction(-2, -1.5, histogram_fraction_4)
+ {} 0.0625
+
+eval instant at 10m histogram_fraction(-8, -1, histogram_fraction_4)
+ {} 0.16666666666666666
+
+eval instant at 10m histogram_fraction(-6, -1, histogram_fraction_4)
+ {} 0.14583333333333334
+
+eval instant at 10m histogram_fraction(-6, -1.5, histogram_fraction_4)
+ {} 0.08333333333333333
+
+eval instant at 10m histogram_fraction(42, 3.1415, histogram_fraction_4)
+ {} 0
+
+eval instant at 10m histogram_fraction(0, 0, histogram_fraction_4)
+ {} 0
+
+eval instant at 10m histogram_fraction(0.000001, 0.000001, histogram_fraction_4)
+ {} 0
+
+eval instant at 10m histogram_fraction(42, 42, histogram_fraction_4)
+ {} 0
+
+eval instant at 10m histogram_fraction(-3.1, -3.1, histogram_fraction_4)
+ {} 0
+
+eval instant at 10m histogram_fraction(3.1415, NaN, histogram_fraction_4)
+ {} NaN
+
+eval instant at 10m histogram_fraction(NaN, 42, histogram_fraction_4)
+ {} NaN
+
+eval instant at 10m histogram_fraction(NaN, NaN, histogram_fraction_4)
+ {} NaN
+
+eval instant at 10m histogram_fraction(-Inf, +Inf, histogram_fraction_4)
+ {} 1
diff --git a/rules/manager_test.go b/rules/manager_test.go
index aeb3276603..2f7343ebb8 100644
--- a/rules/manager_test.go
+++ b/rules/manager_test.go
@@ -2044,7 +2044,7 @@ func TestBoundedRuleEvalConcurrency(t *testing.T) {
require.EqualValues(t, maxInflight.Load(), int32(maxConcurrency)+int32(groupCount))
}
-const artificialDelay = 10 * time.Millisecond
+const artificialDelay = 15 * time.Millisecond
func optsFactory(storage storage.Storage, maxInflight, inflightQueries *atomic.Int32, maxConcurrent int64) *ManagerOptions {
var inflightMu sync.Mutex
diff --git a/storage/remote/azuread/azuread.go b/storage/remote/azuread/azuread.go
index e2058fb54d..58520c6a5d 100644
--- a/storage/remote/azuread/azuread.go
+++ b/storage/remote/azuread/azuread.go
@@ -75,7 +75,7 @@ type AzureADConfig struct { //nolint:revive // exported.
// OAuth is the oauth config that is being used to authenticate.
OAuth *OAuthConfig `yaml:"oauth,omitempty"`
- // OAuth is the oauth config that is being used to authenticate.
+ // SDK is the SDK config that is being used to authenticate.
SDK *SDKConfig `yaml:"sdk,omitempty"`
// Cloud is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina.
diff --git a/tsdb/chunks/head_chunks.go b/tsdb/chunks/head_chunks.go
index 66dbb07b71..6c8707c57b 100644
--- a/tsdb/chunks/head_chunks.go
+++ b/tsdb/chunks/head_chunks.go
@@ -381,6 +381,33 @@ func listChunkFiles(dir string) (map[int]string, error) {
return res, nil
}
+// HardLinkChunkFiles creates hardlinks for chunk files from src to dst.
+// It does nothing if src doesn't exist and ensures dst is created if not.
+func HardLinkChunkFiles(src, dst string) error {
+ _, err := os.Stat(src)
+ if os.IsNotExist(err) {
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("check source chunks dir: %w", err)
+ }
+ if err := os.MkdirAll(dst, 0o777); err != nil {
+ return fmt.Errorf("set up destination chunks dir: %w", err)
+ }
+ files, err := listChunkFiles(src)
+ if err != nil {
+ return fmt.Errorf("list chunks: %w", err)
+ }
+ for _, filePath := range files {
+ _, fileName := filepath.Split(filePath)
+ err := os.Link(filepath.Join(src, fileName), filepath.Join(dst, fileName))
+ if err != nil {
+ return fmt.Errorf("hardlink a chunk: %w", err)
+ }
+ }
+ return nil
+}
+
// repairLastChunkFile deletes the last file if it's empty.
// Because we don't fsync when creating these files, we could end
// up with an empty file at the end during an abrupt shutdown.
diff --git a/tsdb/compact_test.go b/tsdb/compact_test.go
index 10c90e30dc..7a353a556a 100644
--- a/tsdb/compact_test.go
+++ b/tsdb/compact_test.go
@@ -1298,7 +1298,7 @@ func TestCancelCompactions(t *testing.T) {
// This checks that the `context.Canceled` error is properly checked at all levels:
// - tsdb_errors.NewMulti() should have the Is() method implemented for correct checks.
// - callers should check with errors.Is() instead of ==.
- readOnlyDB, err := OpenDBReadOnly(tmpdirCopy, log.NewNopLogger())
+ readOnlyDB, err := OpenDBReadOnly(tmpdirCopy, "", log.NewNopLogger())
require.NoError(t, err)
blocks, err := readOnlyDB.Blocks()
require.NoError(t, err)
diff --git a/tsdb/db.go b/tsdb/db.go
index c2e8904a25..bca3c99480 100644
--- a/tsdb/db.go
+++ b/tsdb/db.go
@@ -383,26 +383,36 @@ var ErrClosed = errors.New("db already closed")
// Current implementation doesn't support concurrency so
// all API calls should happen in the same go routine.
type DBReadOnly struct {
- logger log.Logger
- dir string
- closers []io.Closer
- closed chan struct{}
+ logger log.Logger
+ dir string
+ sandboxDir string
+ closers []io.Closer
+ closed chan struct{}
}
// OpenDBReadOnly opens DB in the given directory for read only operations.
-func OpenDBReadOnly(dir string, l log.Logger) (*DBReadOnly, error) {
+func OpenDBReadOnly(dir, sandboxDirRoot string, l log.Logger) (*DBReadOnly, error) {
if _, err := os.Stat(dir); err != nil {
return nil, fmt.Errorf("opening the db dir: %w", err)
}
+ if sandboxDirRoot == "" {
+ sandboxDirRoot = dir
+ }
+ sandboxDir, err := os.MkdirTemp(sandboxDirRoot, "tmp_dbro_sandbox")
+ if err != nil {
+ return nil, fmt.Errorf("setting up sandbox dir: %w", err)
+ }
+
if l == nil {
l = log.NewNopLogger()
}
return &DBReadOnly{
- logger: l,
- dir: dir,
- closed: make(chan struct{}),
+ logger: l,
+ dir: dir,
+ sandboxDir: sandboxDir,
+ closed: make(chan struct{}),
}, nil
}
@@ -491,7 +501,14 @@ func (db *DBReadOnly) loadDataAsQueryable(maxt int64) (storage.SampleAndChunkQue
}
opts := DefaultHeadOptions()
- opts.ChunkDirRoot = db.dir
+ // Hard link the chunk files to a dir in db.sandboxDir in case the Head needs to truncate some of them
+ // or cut new ones while replaying the WAL.
+ // See https://github.com/prometheus/prometheus/issues/11618.
+ err = chunks.HardLinkChunkFiles(mmappedChunksDir(db.dir), mmappedChunksDir(db.sandboxDir))
+ if err != nil {
+ return nil, err
+ }
+ opts.ChunkDirRoot = db.sandboxDir
head, err := NewHead(nil, db.logger, nil, nil, opts, NewHeadStats())
if err != nil {
return nil, err
@@ -519,7 +536,7 @@ func (db *DBReadOnly) loadDataAsQueryable(maxt int64) (storage.SampleAndChunkQue
}
}
opts := DefaultHeadOptions()
- opts.ChunkDirRoot = db.dir
+ opts.ChunkDirRoot = db.sandboxDir
head, err = NewHead(nil, db.logger, w, wbl, opts, NewHeadStats())
if err != nil {
return nil, err
@@ -690,8 +707,14 @@ func (db *DBReadOnly) Block(blockID string) (BlockReader, error) {
return block, nil
}
-// Close all block readers.
+// Close all block readers and delete the sandbox dir.
func (db *DBReadOnly) Close() error {
+ defer func() {
+ // Delete the temporary sandbox directory that was created when opening the DB.
+ if err := os.RemoveAll(db.sandboxDir); err != nil {
+ level.Error(db.logger).Log("msg", "delete sandbox dir", "err", err)
+ }
+ }()
select {
case <-db.closed:
return ErrClosed
diff --git a/tsdb/db_test.go b/tsdb/db_test.go
index a682f46554..f0b27dcc2a 100644
--- a/tsdb/db_test.go
+++ b/tsdb/db_test.go
@@ -25,6 +25,7 @@ import (
"os"
"path"
"path/filepath"
+ "runtime"
"sort"
"strconv"
"sync"
@@ -2494,7 +2495,7 @@ func TestDBReadOnly(t *testing.T) {
}
// Open a read only db and ensure that the API returns the same result as the normal DB.
- dbReadOnly, err := OpenDBReadOnly(dbDir, logger)
+ dbReadOnly, err := OpenDBReadOnly(dbDir, "", logger)
require.NoError(t, err)
defer func() { require.NoError(t, dbReadOnly.Close()) }()
@@ -2548,10 +2549,14 @@ func TestDBReadOnly(t *testing.T) {
// TestDBReadOnlyClosing ensures that after closing the db
// all api methods return an ErrClosed.
func TestDBReadOnlyClosing(t *testing.T) {
- dbDir := t.TempDir()
- db, err := OpenDBReadOnly(dbDir, log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)))
+ sandboxDir := t.TempDir()
+ db, err := OpenDBReadOnly(t.TempDir(), sandboxDir, log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)))
require.NoError(t, err)
+ // The sandboxDir was there.
+ require.DirExists(t, db.sandboxDir)
require.NoError(t, db.Close())
+ // The sandboxDir was deleted when closing.
+ require.NoDirExists(t, db.sandboxDir)
require.Equal(t, db.Close(), ErrClosed)
_, err = db.Blocks()
require.Equal(t, err, ErrClosed)
@@ -2587,7 +2592,7 @@ func TestDBReadOnly_FlushWAL(t *testing.T) {
}
// Flush WAL.
- db, err := OpenDBReadOnly(dbDir, logger)
+ db, err := OpenDBReadOnly(dbDir, "", logger)
require.NoError(t, err)
flush := t.TempDir()
@@ -2595,7 +2600,7 @@ func TestDBReadOnly_FlushWAL(t *testing.T) {
require.NoError(t, db.Close())
// Reopen the DB from the flushed WAL block.
- db, err = OpenDBReadOnly(flush, logger)
+ db, err = OpenDBReadOnly(flush, "", logger)
require.NoError(t, err)
defer func() { require.NoError(t, db.Close()) }()
blocks, err := db.Blocks()
@@ -2624,6 +2629,80 @@ func TestDBReadOnly_FlushWAL(t *testing.T) {
require.Equal(t, 1000.0, sum)
}
+func TestDBReadOnly_Querier_NoAlteration(t *testing.T) {
+ countChunks := func(dir string) int {
+ files, err := os.ReadDir(mmappedChunksDir(dir))
+ require.NoError(t, err)
+ return len(files)
+ }
+
+ dirHash := func(dir string) (hash []byte) {
+ // Windows requires the DB to be closed: "xxx\lock: The process cannot access the file because it is being used by another process."
+ // But closing the DB alters the directory in this case (it'll cut a new chunk).
+ if runtime.GOOS != "windows" {
+ hash = testutil.DirHash(t, dir)
+ }
+ return
+ }
+
+ spinUpQuerierAndCheck := func(dir, sandboxDir string, chunksCount int) {
+ dBDirHash := dirHash(dir)
+ // Bootsrap a RO db from the same dir and set up a querier.
+ dbReadOnly, err := OpenDBReadOnly(dir, sandboxDir, nil)
+ require.NoError(t, err)
+ require.Equal(t, chunksCount, countChunks(dir))
+ q, err := dbReadOnly.Querier(math.MinInt, math.MaxInt)
+ require.NoError(t, err)
+ require.NoError(t, q.Close())
+ require.NoError(t, dbReadOnly.Close())
+ // The RO Head doesn't alter RW db chunks_head/.
+ require.Equal(t, chunksCount, countChunks(dir))
+ require.Equal(t, dirHash(dir), dBDirHash)
+ }
+
+ t.Run("doesn't cut chunks while replaying WAL", func(t *testing.T) {
+ db := openTestDB(t, nil, nil)
+ defer func() {
+ require.NoError(t, db.Close())
+ }()
+
+ // Append until the first mmaped head chunk.
+ for i := 0; i < 121; i++ {
+ app := db.Appender(context.Background())
+ _, err := app.Append(0, labels.FromStrings("foo", "bar"), int64(i), 0)
+ require.NoError(t, err)
+ require.NoError(t, app.Commit())
+ }
+
+ spinUpQuerierAndCheck(db.dir, t.TempDir(), 0)
+
+ // The RW Head should have no problem cutting its own chunk,
+ // this also proves that a chunk needed to be cut.
+ require.NotPanics(t, func() { db.ForceHeadMMap() })
+ require.Equal(t, 1, countChunks(db.dir))
+ })
+
+ t.Run("doesn't truncate corrupted chunks", func(t *testing.T) {
+ db := openTestDB(t, nil, nil)
+ require.NoError(t, db.Close())
+
+ // Simulate a corrupted chunk: without a header.
+ _, err := os.Create(path.Join(mmappedChunksDir(db.dir), "000001"))
+ require.NoError(t, err)
+
+ spinUpQuerierAndCheck(db.dir, t.TempDir(), 1)
+
+ // The RW Head should have no problem truncating its corrupted file:
+ // this proves that the chunk needed to be truncated.
+ db, err = Open(db.dir, nil, nil, nil, nil)
+ defer func() {
+ require.NoError(t, db.Close())
+ }()
+ require.NoError(t, err)
+ require.Equal(t, 0, countChunks(db.dir))
+ })
+}
+
func TestDBCannotSeePartialCommits(t *testing.T) {
if defaultIsolationDisabled {
t.Skip("skipping test since tsdb isolation is disabled")
diff --git a/tsdb/head.go b/tsdb/head.go
index 8b3d9787ca..d5f7144fdb 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -310,12 +310,22 @@ func (h *Head) resetInMemoryState() error {
return err
}
+ if h.series != nil {
+ // reset the existing series to make sure we call the appropriated hooks
+ // and increment the series removed metrics
+ fs := h.series.iterForDeletion(func(_ int, _ uint64, s *memSeries, flushedForCallback map[chunks.HeadSeriesRef]labels.Labels) {
+ // All series should be flushed
+ flushedForCallback[s.ref] = s.lset
+ })
+ h.metrics.seriesRemoved.Add(float64(fs))
+ }
+
+ h.series = newStripeSeries(h.opts.StripeSize, h.opts.SeriesCallback)
h.iso = newIsolation(h.opts.IsolationDisabled)
h.oooIso = newOOOIsolation()
-
+ h.numSeries.Store(0)
h.exemplarMetrics = em
h.exemplars = es
- h.series = newStripeSeries(h.opts.StripeSize, h.opts.SeriesCallback)
h.postings = index.NewUnorderedMemPostings()
h.tombstones = tombstones.NewMemTombstones()
h.deleted = map[chunks.HeadSeriesRef]int{}
@@ -1861,11 +1871,10 @@ func newStripeSeries(stripeSize int, seriesCallback SeriesLifecycleCallback) *st
// minMmapFile is the min mmap file number seen in the series (in-order and out-of-order) after gc'ing the series.
func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (_ map[storage.SeriesRef]struct{}, _ int, _, _ int64, minMmapFile int) {
var (
- deleted = map[storage.SeriesRef]struct{}{}
- rmChunks = 0
- actualMint int64 = math.MaxInt64
- minOOOTime int64 = math.MaxInt64
- deletedFromPrevStripe = 0
+ deleted = map[storage.SeriesRef]struct{}{}
+ rmChunks = 0
+ actualMint int64 = math.MaxInt64
+ minOOOTime int64 = math.MaxInt64
)
minMmapFile = math.MaxInt32
@@ -1923,27 +1932,7 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (
deletedForCallback[series.ref] = series.lset
}
- // Run through all series shard by shard, checking which should be deleted.
- for i := 0; i < s.size; i++ {
- deletedForCallback := make(map[chunks.HeadSeriesRef]labels.Labels, deletedFromPrevStripe)
- s.locks[i].Lock()
-
- // Delete conflicts first so seriesHashmap.del doesn't move them to the `unique` field,
- // after deleting `unique`.
- for hash, all := range s.hashes[i].conflicts {
- for _, series := range all {
- check(i, hash, series, deletedForCallback)
- }
- }
- for hash, series := range s.hashes[i].unique {
- check(i, hash, series, deletedForCallback)
- }
-
- s.locks[i].Unlock()
-
- s.seriesLifecycleCallback.PostDeletion(deletedForCallback)
- deletedFromPrevStripe = len(deletedForCallback)
- }
+ s.iterForDeletion(check)
if actualMint == math.MaxInt64 {
actualMint = mint
@@ -1952,6 +1941,35 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (
return deleted, rmChunks, actualMint, minOOOTime, minMmapFile
}
+// The iterForDeletion function iterates through all series, invoking the checkDeletedFunc for each.
+// The checkDeletedFunc takes a map as input and should add to it all series that were deleted and should be included
+// when invoking the PostDeletion hook.
+func (s *stripeSeries) iterForDeletion(checkDeletedFunc func(int, uint64, *memSeries, map[chunks.HeadSeriesRef]labels.Labels)) int {
+ seriesSetFromPrevStripe := 0
+ totalDeletedSeries := 0
+ // Run through all series shard by shard
+ for i := 0; i < s.size; i++ {
+ seriesSet := make(map[chunks.HeadSeriesRef]labels.Labels, seriesSetFromPrevStripe)
+ s.locks[i].Lock()
+ // Iterate conflicts first so f doesn't move them to the `unique` field,
+ // after deleting `unique`.
+ for hash, all := range s.hashes[i].conflicts {
+ for _, series := range all {
+ checkDeletedFunc(i, hash, series, seriesSet)
+ }
+ }
+
+ for hash, series := range s.hashes[i].unique {
+ checkDeletedFunc(i, hash, series, seriesSet)
+ }
+ s.locks[i].Unlock()
+ s.seriesLifecycleCallback.PostDeletion(seriesSet)
+ totalDeletedSeries += len(seriesSet)
+ seriesSetFromPrevStripe = len(seriesSet)
+ }
+ return totalDeletedSeries
+}
+
func (s *stripeSeries) getByID(id chunks.HeadSeriesRef) *memSeries {
i := uint64(id) & uint64(s.size-1)
diff --git a/tsdb/head_test.go b/tsdb/head_test.go
index 804886ad7b..6b4ec4ca41 100644
--- a/tsdb/head_test.go
+++ b/tsdb/head_test.go
@@ -4007,6 +4007,9 @@ func TestSnapshotError(t *testing.T) {
require.NoError(t, err)
f, err := os.OpenFile(path.Join(snapDir, files[0].Name()), os.O_RDWR, 0)
require.NoError(t, err)
+ // Create snapshot backup to be restored on future test cases.
+ snapshotBackup, err := io.ReadAll(f)
+ require.NoError(t, err)
_, err = f.WriteAt([]byte{0b11111111}, 18)
require.NoError(t, err)
require.NoError(t, f.Close())
@@ -4021,10 +4024,44 @@ func TestSnapshotError(t *testing.T) {
// There should be no series in the memory after snapshot error since WAL was removed.
require.Equal(t, 1.0, prom_testutil.ToFloat64(head.metrics.snapshotReplayErrorTotal))
+ require.Equal(t, uint64(0), head.NumSeries())
require.Nil(t, head.series.getByHash(lbls.Hash(), lbls))
tm, err = head.tombstones.Get(1)
require.NoError(t, err)
require.Empty(t, tm)
+ require.NoError(t, head.Close())
+
+ // Test corruption in the middle of the snapshot.
+ f, err = os.OpenFile(path.Join(snapDir, files[0].Name()), os.O_RDWR, 0)
+ require.NoError(t, err)
+ _, err = f.WriteAt(snapshotBackup, 0)
+ require.NoError(t, err)
+ _, err = f.WriteAt([]byte{0b11111111}, 300)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+
+ c := &countSeriesLifecycleCallback{}
+ opts := head.opts
+ opts.SeriesCallback = c
+
+ w, err = wlog.NewSize(nil, nil, head.wal.Dir(), 32768, wlog.CompressionNone)
+ require.NoError(t, err)
+ head, err = NewHead(prometheus.NewRegistry(), nil, w, nil, head.opts, nil)
+ require.NoError(t, err)
+ require.NoError(t, head.Init(math.MinInt64))
+
+ // There should be no series in the memory after snapshot error since WAL was removed.
+ require.Equal(t, 1.0, prom_testutil.ToFloat64(head.metrics.snapshotReplayErrorTotal))
+ require.Nil(t, head.series.getByHash(lbls.Hash(), lbls))
+ require.Equal(t, uint64(0), head.NumSeries())
+
+ // Since the snapshot could replay certain series, we continue invoking the create hooks.
+ // In such instances, we need to ensure that we also trigger the delete hooks when resetting the memory.
+ require.Equal(t, int64(2), c.created.Load())
+ require.Equal(t, int64(2), c.deleted.Load())
+
+ require.Equal(t, 2.0, prom_testutil.ToFloat64(head.metrics.seriesRemoved))
+ require.Equal(t, 2.0, prom_testutil.ToFloat64(head.metrics.seriesCreated))
}
func TestHistogramMetrics(t *testing.T) {
@@ -5829,3 +5866,14 @@ func TestHeadCompactableDoesNotCompactEmptyHead(t *testing.T) {
require.False(t, head.compactable())
}
+
+type countSeriesLifecycleCallback struct {
+ created atomic.Int64
+ deleted atomic.Int64
+}
+
+func (c *countSeriesLifecycleCallback) PreCreation(labels.Labels) error { return nil }
+func (c *countSeriesLifecycleCallback) PostCreation(labels.Labels) { c.created.Inc() }
+func (c *countSeriesLifecycleCallback) PostDeletion(s map[chunks.HeadSeriesRef]labels.Labels) {
+ c.deleted.Add(int64(len(s)))
+}
diff --git a/tsdb/index/index.go b/tsdb/index/index.go
index 4ded4cbe20..480e6a8fc7 100644
--- a/tsdb/index/index.go
+++ b/tsdb/index/index.go
@@ -53,7 +53,7 @@ const (
seriesByteAlign = 16
// checkContextEveryNIterations is used in some tight loops to check if the context is done.
- checkContextEveryNIterations = 100
+ checkContextEveryNIterations = 128
)
type indexWriterSeries struct {
diff --git a/tsdb/index/index_test.go b/tsdb/index/index_test.go
index 22133d0b70..5c6d64e076 100644
--- a/tsdb/index/index_test.go
+++ b/tsdb/index/index_test.go
@@ -20,7 +20,9 @@ import (
"hash/crc32"
"os"
"path/filepath"
+ "slices"
"sort"
+ "strconv"
"testing"
"github.com/stretchr/testify/require"
@@ -160,39 +162,14 @@ func TestIndexRW_Create_Open(t *testing.T) {
}
func TestIndexRW_Postings(t *testing.T) {
- dir := t.TempDir()
ctx := context.Background()
-
- fn := filepath.Join(dir, indexFilename)
-
- iw, err := NewWriter(context.Background(), fn)
- require.NoError(t, err)
-
- series := []labels.Labels{
- labels.FromStrings("a", "1", "b", "1"),
- labels.FromStrings("a", "1", "b", "2"),
- labels.FromStrings("a", "1", "b", "3"),
- labels.FromStrings("a", "1", "b", "4"),
+ var input indexWriterSeriesSlice
+ for i := 1; i < 5; i++ {
+ input = append(input, &indexWriterSeries{
+ labels: labels.FromStrings("a", "1", "b", strconv.Itoa(i)),
+ })
}
-
- require.NoError(t, iw.AddSymbol("1"))
- require.NoError(t, iw.AddSymbol("2"))
- require.NoError(t, iw.AddSymbol("3"))
- require.NoError(t, iw.AddSymbol("4"))
- require.NoError(t, iw.AddSymbol("a"))
- require.NoError(t, iw.AddSymbol("b"))
-
- // Postings lists are only written if a series with the respective
- // reference was added before.
- require.NoError(t, iw.AddSeries(1, series[0]))
- require.NoError(t, iw.AddSeries(2, series[1]))
- require.NoError(t, iw.AddSeries(3, series[2]))
- require.NoError(t, iw.AddSeries(4, series[3]))
-
- require.NoError(t, iw.Close())
-
- ir, err := NewFileReader(fn)
- require.NoError(t, err)
+ ir, fn, _ := createFileReader(ctx, t, input)
p, err := ir.Postings(ctx, "a", "1")
require.NoError(t, err)
@@ -205,7 +182,7 @@ func TestIndexRW_Postings(t *testing.T) {
require.NoError(t, err)
require.Empty(t, c)
- testutil.RequireEqual(t, series[i], builder.Labels())
+ testutil.RequireEqual(t, input[i].labels, builder.Labels())
}
require.NoError(t, p.Err())
@@ -240,8 +217,6 @@ func TestIndexRW_Postings(t *testing.T) {
"b": {"1", "2", "3", "4"},
}, labelIndices)
- require.NoError(t, ir.Close())
-
t.Run("ShardedPostings()", func(t *testing.T) {
ir, err := NewFileReader(fn)
require.NoError(t, err)
@@ -296,42 +271,16 @@ func TestIndexRW_Postings(t *testing.T) {
}
func TestPostingsMany(t *testing.T) {
- dir := t.TempDir()
ctx := context.Background()
-
- fn := filepath.Join(dir, indexFilename)
-
- iw, err := NewWriter(context.Background(), fn)
- require.NoError(t, err)
-
// Create a label in the index which has 999 values.
- symbols := map[string]struct{}{}
- series := []labels.Labels{}
+ var input indexWriterSeriesSlice
for i := 1; i < 1000; i++ {
v := fmt.Sprintf("%03d", i)
- series = append(series, labels.FromStrings("i", v, "foo", "bar"))
- symbols[v] = struct{}{}
+ input = append(input, &indexWriterSeries{
+ labels: labels.FromStrings("i", v, "foo", "bar"),
+ })
}
- symbols["i"] = struct{}{}
- symbols["foo"] = struct{}{}
- symbols["bar"] = struct{}{}
- syms := []string{}
- for s := range symbols {
- syms = append(syms, s)
- }
- sort.Strings(syms)
- for _, s := range syms {
- require.NoError(t, iw.AddSymbol(s))
- }
-
- for i, s := range series {
- require.NoError(t, iw.AddSeries(storage.SeriesRef(i), s))
- }
- require.NoError(t, iw.Close())
-
- ir, err := NewFileReader(fn)
- require.NoError(t, err)
- defer func() { require.NoError(t, ir.Close()) }()
+ ir, _, symbols := createFileReader(ctx, t, input)
cases := []struct {
in []string
@@ -387,25 +336,13 @@ func TestPostingsMany(t *testing.T) {
}
func TestPersistence_index_e2e(t *testing.T) {
- dir := t.TempDir()
ctx := context.Background()
-
lbls, err := labels.ReadLabels(filepath.Join("..", "testdata", "20kseries.json"), 20000)
require.NoError(t, err)
-
// Sort labels as the index writer expects series in sorted order.
sort.Sort(labels.Slice(lbls))
- symbols := map[string]struct{}{}
- for _, lset := range lbls {
- lset.Range(func(l labels.Label) {
- symbols[l.Name] = struct{}{}
- symbols[l.Value] = struct{}{}
- })
- }
-
var input indexWriterSeriesSlice
-
ref := uint64(0)
// Generate ChunkMetas for every label set.
for i, lset := range lbls {
@@ -426,17 +363,7 @@ func TestPersistence_index_e2e(t *testing.T) {
})
}
- iw, err := NewWriter(context.Background(), filepath.Join(dir, indexFilename))
- require.NoError(t, err)
-
- syms := []string{}
- for s := range symbols {
- syms = append(syms, s)
- }
- sort.Strings(syms)
- for _, s := range syms {
- require.NoError(t, iw.AddSymbol(s))
- }
+ ir, _, _ := createFileReader(ctx, t, input)
// Population procedure as done by compaction.
var (
@@ -447,8 +374,6 @@ func TestPersistence_index_e2e(t *testing.T) {
mi := newMockIndex()
for i, s := range input {
- err = iw.AddSeries(storage.SeriesRef(i), s.labels, s.chunks...)
- require.NoError(t, err)
require.NoError(t, mi.AddSeries(storage.SeriesRef(i), s.labels, s.chunks...))
s.labels.Range(func(l labels.Label) {
@@ -462,12 +387,6 @@ func TestPersistence_index_e2e(t *testing.T) {
postings.Add(storage.SeriesRef(i), s.labels)
}
- err = iw.Close()
- require.NoError(t, err)
-
- ir, err := NewFileReader(filepath.Join(dir, indexFilename))
- require.NoError(t, err)
-
for p := range mi.postings {
gotp, err := ir.Postings(ctx, p.Name, p.Value)
require.NoError(t, err)
@@ -523,8 +442,6 @@ func TestPersistence_index_e2e(t *testing.T) {
}
sort.Strings(expSymbols)
require.Equal(t, expSymbols, gotSymbols)
-
- require.NoError(t, ir.Close())
}
func TestWriter_ShouldReturnErrorOnSeriesWithDuplicatedLabelNames(t *testing.T) {
@@ -624,39 +541,14 @@ func BenchmarkReader_ShardedPostings(b *testing.B) {
numShards = 16
)
- dir, err := os.MkdirTemp("", "benchmark_reader_sharded_postings")
- require.NoError(b, err)
- defer func() {
- require.NoError(b, os.RemoveAll(dir))
- }()
-
ctx := context.Background()
-
- // Generate an index.
- fn := filepath.Join(dir, indexFilename)
-
- iw, err := NewWriter(ctx, fn)
- require.NoError(b, err)
-
+ var input indexWriterSeriesSlice
for i := 1; i <= numSeries; i++ {
- require.NoError(b, iw.AddSymbol(fmt.Sprintf("%10d", i)))
+ input = append(input, &indexWriterSeries{
+ labels: labels.FromStrings("const", fmt.Sprintf("%10d", 1), "unique", fmt.Sprintf("%10d", i)),
+ })
}
- require.NoError(b, iw.AddSymbol("const"))
- require.NoError(b, iw.AddSymbol("unique"))
-
- for i := 1; i <= numSeries; i++ {
- require.NoError(b, iw.AddSeries(storage.SeriesRef(i),
- labels.FromStrings("const", fmt.Sprintf("%10d", 1), "unique", fmt.Sprintf("%10d", i))))
- }
-
- require.NoError(b, iw.Close())
-
- b.ResetTimer()
-
- // Create a reader to read back all postings from the index.
- ir, err := NewFileReader(fn)
- require.NoError(b, err)
-
+ ir, _, _ := createFileReader(ctx, b, input)
b.ResetTimer()
for n := 0; n < b.N; n++ {
@@ -721,28 +613,17 @@ func TestChunksTimeOrdering(t *testing.T) {
}
func TestReader_PostingsForLabelMatchingHonorsContextCancel(t *testing.T) {
- dir := t.TempDir()
-
- idx, err := NewWriter(context.Background(), filepath.Join(dir, "index"))
- require.NoError(t, err)
-
- seriesCount := 1000
- for i := 1; i <= seriesCount; i++ {
- require.NoError(t, idx.AddSymbol(fmt.Sprintf("%4d", i)))
+ const seriesCount = 1000
+ var input indexWriterSeriesSlice
+ for i := 1; i < seriesCount; i++ {
+ input = append(input, &indexWriterSeries{
+ labels: labels.FromStrings("__name__", fmt.Sprintf("%4d", i)),
+ chunks: []chunks.Meta{
+ {Ref: 1, MinTime: 0, MaxTime: 10},
+ },
+ })
}
- require.NoError(t, idx.AddSymbol("__name__"))
-
- for i := 1; i <= seriesCount; i++ {
- require.NoError(t, idx.AddSeries(storage.SeriesRef(i), labels.FromStrings("__name__", fmt.Sprintf("%4d", i)),
- chunks.Meta{Ref: 1, MinTime: 0, MaxTime: 10},
- ))
- }
-
- require.NoError(t, idx.Close())
-
- ir, err := NewFileReader(filepath.Join(dir, "index"))
- require.NoError(t, err)
- defer ir.Close()
+ ir, _, _ := createFileReader(context.Background(), t, input)
failAfter := uint64(seriesCount / 2) // Fail after processing half of the series.
ctx := &testutil.MockContextErrAfter{FailAfter: failAfter}
@@ -752,3 +633,42 @@ func TestReader_PostingsForLabelMatchingHonorsContextCancel(t *testing.T) {
require.Error(t, p.Err())
require.Equal(t, failAfter, ctx.Count())
}
+
+// createFileReader creates a temporary index file. It writes the provided input to this file.
+// It returns a Reader for this file, the file's name, and the symbol map.
+func createFileReader(ctx context.Context, tb testing.TB, input indexWriterSeriesSlice) (*Reader, string, map[string]struct{}) {
+ tb.Helper()
+
+ fn := filepath.Join(tb.TempDir(), indexFilename)
+
+ iw, err := NewWriter(ctx, fn)
+ require.NoError(tb, err)
+
+ symbols := map[string]struct{}{}
+ for _, s := range input {
+ s.labels.Range(func(l labels.Label) {
+ symbols[l.Name] = struct{}{}
+ symbols[l.Value] = struct{}{}
+ })
+ }
+
+ syms := []string{}
+ for s := range symbols {
+ syms = append(syms, s)
+ }
+ slices.Sort(syms)
+ for _, s := range syms {
+ require.NoError(tb, iw.AddSymbol(s))
+ }
+ for i, s := range input {
+ require.NoError(tb, iw.AddSeries(storage.SeriesRef(i), s.labels, s.chunks...))
+ }
+ require.NoError(tb, iw.Close())
+
+ ir, err := NewFileReader(fn)
+ require.NoError(tb, err)
+ tb.Cleanup(func() {
+ require.NoError(tb, ir.Close())
+ })
+ return ir, fn, symbols
+}
diff --git a/tsdb/index/postings_test.go b/tsdb/index/postings_test.go
index 7fa0a892b9..2cbc14ac64 100644
--- a/tsdb/index/postings_test.go
+++ b/tsdb/index/postings_test.go
@@ -22,8 +22,10 @@ import (
"math/rand"
"sort"
"strconv"
+ "strings"
"testing"
+ "github.com/grafana/regexp"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/labels"
@@ -1284,6 +1286,58 @@ func BenchmarkListPostings(b *testing.B) {
}
}
+func slowRegexpString() string {
+ nums := map[int]struct{}{}
+ for i := 10_000; i < 20_000; i++ {
+ if i%3 == 0 {
+ nums[i] = struct{}{}
+ }
+ }
+
+ var sb strings.Builder
+ sb.WriteString(".*(9999")
+ for i := range nums {
+ sb.WriteString("|")
+ sb.WriteString(strconv.Itoa(i))
+ }
+ sb.WriteString(").*")
+ return sb.String()
+}
+
+func BenchmarkMemPostings_PostingsForLabelMatching(b *testing.B) {
+ fast := regexp.MustCompile("^(100|200)$")
+ slowRegexp := "^" + slowRegexpString() + "$"
+ b.Logf("Slow regexp length = %d", len(slowRegexp))
+ slow := regexp.MustCompile(slowRegexp)
+
+ for _, labelValueCount := range []int{1_000, 10_000, 100_000} {
+ b.Run(fmt.Sprintf("labels=%d", labelValueCount), func(b *testing.B) {
+ mp := NewMemPostings()
+ for i := 0; i < labelValueCount; i++ {
+ mp.Add(storage.SeriesRef(i), labels.FromStrings("label", strconv.Itoa(i)))
+ }
+
+ fp, err := ExpandPostings(mp.PostingsForLabelMatching(context.Background(), "label", fast.MatchString))
+ require.NoError(b, err)
+ b.Logf("Fast matcher matches %d series", len(fp))
+ b.Run("matcher=fast", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ mp.PostingsForLabelMatching(context.Background(), "label", fast.MatchString).Next()
+ }
+ })
+
+ sp, err := ExpandPostings(mp.PostingsForLabelMatching(context.Background(), "label", slow.MatchString))
+ require.NoError(b, err)
+ b.Logf("Slow matcher matches %d series", len(sp))
+ b.Run("matcher=slow", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ mp.PostingsForLabelMatching(context.Background(), "label", slow.MatchString).Next()
+ }
+ })
+ })
+ }
+}
+
func TestMemPostings_PostingsForLabelMatchingHonorsContextCancel(t *testing.T) {
memP := NewMemPostings()
seriesCount := 10 * checkContextEveryNIterations
diff --git a/web/api/v1/api.go b/web/api/v1/api.go
index dc22365073..f0884926e1 100644
--- a/web/api/v1/api.go
+++ b/web/api/v1/api.go
@@ -116,9 +116,11 @@ type RulesRetriever interface {
AlertingRules() []*rules.AlertingRule
}
+// StatsRenderer converts engine statistics into a format suitable for the API.
type StatsRenderer func(context.Context, *stats.Statistics, string) stats.QueryStats
-func defaultStatsRenderer(_ context.Context, s *stats.Statistics, param string) stats.QueryStats {
+// DefaultStatsRenderer is the default stats renderer for the API.
+func DefaultStatsRenderer(_ context.Context, s *stats.Statistics, param string) stats.QueryStats {
if param != "" {
return stats.NewQueryStats(s)
}
@@ -272,7 +274,7 @@ func NewAPI(
buildInfo: buildInfo,
gatherer: gatherer,
isAgent: isAgent,
- statsRenderer: defaultStatsRenderer,
+ statsRenderer: DefaultStatsRenderer,
remoteReadHandler: remote.NewReadHandler(logger, registerer, q, configFunc, remoteReadSampleLimit, remoteReadConcurrencyLimit, remoteReadMaxBytesInFrame),
}
@@ -461,7 +463,7 @@ func (api *API) query(r *http.Request) (result apiFuncResult) {
// Optional stats field in response if parameter "stats" is not empty.
sr := api.statsRenderer
if sr == nil {
- sr = defaultStatsRenderer
+ sr = DefaultStatsRenderer
}
qs := sr(ctx, qry.Stats(), r.FormValue("stats"))
@@ -563,7 +565,7 @@ func (api *API) queryRange(r *http.Request) (result apiFuncResult) {
// Optional stats field in response if parameter "stats" is not empty.
sr := api.statsRenderer
if sr == nil {
- sr = defaultStatsRenderer
+ sr = DefaultStatsRenderer
}
qs := sr(ctx, qry.Stats(), r.FormValue("stats"))
@@ -702,7 +704,7 @@ func (api *API) labelNames(r *http.Request) apiFuncResult {
names = []string{}
}
- if len(names) >= limit {
+ if len(names) > limit {
names = names[:limit]
warnings = warnings.Add(errors.New("results truncated due to limit"))
}
@@ -791,7 +793,7 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
slices.Sort(vals)
- if len(vals) >= limit {
+ if len(vals) > limit {
vals = vals[:limit]
warnings = warnings.Add(errors.New("results truncated due to limit"))
}
@@ -887,7 +889,8 @@ func (api *API) series(r *http.Request) (result apiFuncResult) {
}
metrics = append(metrics, set.At().Labels())
- if len(metrics) >= limit {
+ if len(metrics) > limit {
+ metrics = metrics[:limit]
warnings.Add(errors.New("results truncated due to limit"))
return apiFuncResult{metrics, nil, warnings, closer}
}
diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go
index 7d55dd11a0..74cd2239d5 100644
--- a/web/api/v1/api_test.go
+++ b/web/api/v1/api_test.go
@@ -1060,6 +1060,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
responseLen int // If nonzero, check only the length; `response` is ignored.
responseMetadataTotal int
responseAsJSON string
+ warningsCount int
errType errorType
sorter func(interface{})
metadata []targetMetadata
@@ -1417,7 +1418,17 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
"match[]": []string{"test_metric1"},
"limit": []string{"1"},
},
- responseLen: 1, // API does not specify which particular value will come back.
+ responseLen: 1, // API does not specify which particular value will come back.
+ warningsCount: 1,
+ },
+ {
+ endpoint: api.series,
+ query: url.Values{
+ "match[]": []string{"test_metric1"},
+ "limit": []string{"2"},
+ },
+ responseLen: 2, // API does not specify which particular value will come back.
+ warningsCount: 0, // No warnings if limit isn't exceeded.
},
// Missing match[] query params in series requests.
{
@@ -2700,7 +2711,19 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
query: url.Values{
"limit": []string{"2"},
},
- responseLen: 2, // API does not specify which particular values will come back.
+ responseLen: 2, // API does not specify which particular values will come back.
+ warningsCount: 1,
+ },
+ {
+ endpoint: api.labelValues,
+ params: map[string]string{
+ "name": "__name__",
+ },
+ query: url.Values{
+ "limit": []string{"4"},
+ },
+ responseLen: 4, // API does not specify which particular values will come back.
+ warningsCount: 0, // No warnings if limit isn't exceeded.
},
// Label names.
{
@@ -2847,7 +2870,16 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
query: url.Values{
"limit": []string{"2"},
},
- responseLen: 2, // API does not specify which particular values will come back.
+ responseLen: 2, // API does not specify which particular values will come back.
+ warningsCount: 1,
+ },
+ {
+ endpoint: api.labelNames,
+ query: url.Values{
+ "limit": []string{"3"},
+ },
+ responseLen: 3, // API does not specify which particular values will come back.
+ warningsCount: 0, // No warnings if limit isn't exceeded.
},
}...)
}
@@ -2924,6 +2956,8 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
require.NoError(t, err)
require.JSONEq(t, test.responseAsJSON, string(s))
}
+
+ require.Len(t, res.warnings, test.warningsCount)
})
}
})