diff --git a/exporter/fixtures_test.go b/exporter/fixtures_test.go new file mode 100644 index 000000000..6b433247c --- /dev/null +++ b/exporter/fixtures_test.go @@ -0,0 +1,95 @@ +// mongodb_exporter +// Copyright (C) 2017 Percona LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exporter + +import ( + "os" + "testing" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" +) + +// diagnosticData83FixturePath is a getDiagnosticData reply captured from the MongoDB 8.3.2 +// instance of https://github.com/percona/mongodb_exporter/issues/1285, trimmed to the +// subtrees the tests below need. +const diagnosticData83FixturePath = "testdata/get_diagnostic_data_8.3.json" + +// loadDiagnosticData83Fixture reads the captured command reply. Extended JSON is used instead of +// encoding/json because the driver decodes arrays into primitive.A while encoding/json +// produces []any, which makeMetrics silently walks past. A fixture parsed the plain way +// hides everything array shaped, histogram buckets included. +func loadDiagnosticData83Fixture(t *testing.T) bson.M { + t.Helper() + + buf, err := os.ReadFile(diagnosticData83FixturePath) + require.NoError(t, err) + + var m bson.M + require.NoError(t, bson.UnmarshalExtJSON(buf, false, &m)) + + return m +} + +// gatherMetrics exports the metrics the way the registry does at scrape time, so duplicated +// series and inconsistent descriptors fail the test instead of the scrape. +func gatherMetrics(t *testing.T, metrics []prometheus.Metric) map[string]*dto.MetricFamily { + t.Helper() + + reg := prometheus.NewPedanticRegistry() + require.NoError(t, reg.Register(staticCollector(metrics)), "descriptors must be consistent") + + gatheredMetrics, err := reg.Gather() + require.NoError(t, err, "metrics must be exported only once") + + metricsByName := make(map[string]*dto.MetricFamily, len(gatheredMetrics)) + for _, metric := range gatheredMetrics { + metricsByName[metric.GetName()] = metric + } + + return metricsByName +} + +// labelValues returns the value of the given label for every series of a metric family. +func labelValues(family *dto.MetricFamily, name string) []string { + values := make([]string, 0, len(family.GetMetric())) + for _, metric := range family.GetMetric() { + for _, label := range metric.GetLabel() { + if label.GetName() == name { + values = append(values, label.GetValue()) + } + } + } + + return values +} + +// valuesByLabel returns the value of every series of a metric family, keyed by the value the +// given label has on that series. +func valuesByLabel(family *dto.MetricFamily, name string) map[string]float64 { + values := make(map[string]float64, len(family.GetMetric())) + for _, metric := range family.GetMetric() { + for _, label := range metric.GetLabel() { + if label.GetName() == name { + values[label.GetValue()] = metric.GetUntyped().GetValue() + } + } + } + + return values +} diff --git a/exporter/metrics.go b/exporter/metrics.go index 3ff63a90f..a73f71c96 100644 --- a/exporter/metrics.go +++ b/exporter/metrics.go @@ -32,6 +32,10 @@ import ( const ( exporterPrefix = "mongodb_" + + // collisionLabel carries the document field a metric was built from. It is set only when + // several fields of one document map to the same metric name, and it keeps them apart. + collisionLabel = "metric_field" ) type rawMetric struct { @@ -318,20 +322,33 @@ func makeMetricsWithHistograms(prefix string, m bson.M, labels map[string]string prefix += "." } + // Fields that are exported as a label value never become a part of the metric name, so they + // cannot collide. Indexing them anyway would give the collision label to a part of the + // family only, which the registry rejects as inconsistent label names. + var colliding map[string]string + if !keyBecomesLabelValue(prefix) { + colliding = collidingFields(prefix, m) + } + for k, val := range m { - nextPrefix := prefix + k + // A field sharing its metric name with a sibling is exported under the name of the + // canonical field of the group, so that the whole group agrees on the name and on the + // help. The collision label carries the real field and keeps the series apart. + name, l := k, labels + if canonical, ok := colliding[k]; ok { + name, l = canonical, withLabel(labels, collisionLabel, k) + } + + nextPrefix := prefix + name if !includeHistograms && isHistogramPath(nextPrefix) { continue } - l := make(map[string]string) if label, ok := keyNodesToLabels[prefix]; ok { - maps.Copy(l, labels) - l[label] = k + l = withLabel(l, label, k) nextPrefix = prefix + label - } else { - l = labels } + switch v := val.(type) { case bson.M: res = append(res, makeMetricsWithHistograms(nextPrefix, v, l, compatibleMode, includeHistograms)...) @@ -345,13 +362,111 @@ func makeMetricsWithHistograms(prefix string, m bson.M, labels map[string]string } continue default: - res = appendMetricValue(res, prefix, k, v, l, compatibleMode) + res = appendMetricValue(res, prefix, name, v, l, compatibleMode) } } return res } +// keyBecomesLabelValue reports whether the fields of a document under prefix are exported as the +// value of a label instead of as a part of the metric name. +func keyBecomesLabelValue(prefix string) bool { + if _, ok := nodeToPDMetrics[prefix]; ok { + return true + } + + _, ok := keyNodesToLabels[prefix] + + return ok +} + +// collidingFields maps every field of a document that does not end up with a unique metric name +// to the canonical field of its group. MongoDB can report several fields whose names differ only +// in characters that prometheusize collapses, for example the "giant hdr" counters that ethtool +// exposes with a different amount of leading spaces. Exported as they are, they would be the very +// same series and the registry would reject the whole scrape, so the group is exported under one +// name and the collision label keeps its members apart. The name is built with prometheusize +// itself, because anything less than the full mapping misses collisions: fields differing only in +// a trailing special character or in a run of underscores share a name too. The canonical field is +// the lowest one, because the BSON document order is lost when it is decoded into a map and the +// name has to stay the same between scrapes. +func collidingFields(prefix string, m bson.M) map[string]string { + if len(m) <= 1 || allKeysUnambiguous(m) { + return nil + } + + fieldsByMetricName := make(map[string][]string, len(m)) + for k := range m { + metricName := prometheusize(prefix + k) + fieldsByMetricName[metricName] = append(fieldsByMetricName[metricName], k) + } + + var canonical map[string]string + for _, fields := range fieldsByMetricName { + if len(fields) == 1 { + continue + } + + if canonical == nil { + canonical = make(map[string]string, len(fields)) + } + first := slices.Min(fields) + for _, k := range fields { + canonical[k] = first + } + } + + return canonical +} + +// allKeysUnambiguous reports whether every key of a document already looks like the metric name +// part it maps to. Such keys cannot collide with each other, because prometheusize leaves their +// tail untouched and a document cannot hold the same key twice. Almost everything MongoDB +// reports qualifies, so this keeps the collision check off the hot path. +func allKeysUnambiguous(m bson.M) bool { + for k := range m { + if !isUnambiguousKey(k) { + return false + } + } + + return true +} + +// isUnambiguousKey reports whether k passes through prometheusize unchanged, that is whether it +// is a non empty sequence of alphanumeric groups joined by single underscores. Anything else can +// be mapped onto another key: special characters and underscore runs collapse into a single +// underscore, and a leading or trailing underscore is dropped. +func isUnambiguousKey(k string) bool { + previousIsUnderscore := true + for i := range len(k) { + switch c := k[i]; { + case c == '_': + if previousIsUnderscore { + return false + } + previousIsUnderscore = true + case c >= '0' && c <= '9', c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z': + previousIsUnderscore = false + default: + return false + } + } + + return !previousIsUnderscore +} + +// withLabel returns a copy of labels with one more label set, so that the map a document shares +// with its siblings is never modified in place. +func withLabel(labels map[string]string, name, value string) map[string]string { + extended := make(map[string]string, len(labels)+1) + maps.Copy(extended, labels) + extended[name] = value + + return extended +} + // Extract maps from arrays. Only some structures like replicasets have arrays of members // and each member is represented by a map[string]any. func processSlice(prefix string, v []any, commonLabels map[string]string, compatibleMode, includeHistograms bool) []prometheus.Metric { diff --git a/exporter/metrics_test.go b/exporter/metrics_test.go index a9abdbff8..912baf594 100644 --- a/exporter/metrics_test.go +++ b/exporter/metrics_test.go @@ -23,6 +23,7 @@ import ( "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) @@ -305,6 +306,158 @@ func TestHistogramMetricsAreSkippedByDefault(t *testing.T) { assert.Empty(t, metrics) } +// ethtool reports several counters per queue whose names differ only in leading spaces, +// which used to produce one metric name with two different help strings. +func TestKeysCollidingAfterSanitizationStayDistinct(t *testing.T) { + t.Parallel() + + metricsByName := gatherMetrics(t, makeMetrics("systemMetrics.ethtool", bson.M{ + "ens192": bson.M{ + " giant hdr": int64(11), + " giant hdr": int64(22), + "tx queue": int64(33), + }, + }, nil, false)) + + assert.Contains(t, metricsByName, "mongodb_sys_ethtool_ens192_tx_queue") + + collided, ok := metricsByName["mongodb_sys_ethtool_ens192_giant_hdr"] + require.True(t, ok) + + // The label carries the field the value came from, so the series stay identifiable and do + // not get renumbered when MongoDB stops reporting one of them. + assert.Equal(t, map[string]float64{ + " giant hdr": 11, + " giant hdr": 22, + }, valuesByLabel(collided, collisionLabel)) +} + +// Sanitization collapses every special character, not only whitespace, so fields differing in any +// of them share one metric name and therefore have to share one help string as well. +func TestCollidingFieldsShareOneDescriptor(t *testing.T) { + t.Parallel() + + metrics := makeMetrics("systemMetrics.ethtool", bson.M{ + "ens192": bson.M{ + "giant hdr": int64(11), + "giant-hdr": int64(22), + "giant hdr#": int64(33), + }, + }, nil, false) + + collided, ok := gatherMetrics(t, metrics)["mongodb_sys_ethtool_ens192_giant_hdr"] + require.True(t, ok) + assert.Equal(t, "systemMetrics.ethtool.ens192.giant hdr", collided.GetHelp()) + assert.ElementsMatch(t, []string{"giant hdr", "giant-hdr", "giant hdr#"}, + labelValues(collided, collisionLabel)) +} + +// The vmxnet3 NIC of the reported host exposes two "giant hdr" counters that differ only +// in leading whitespace, which used to make the whole systemMetrics tree unexportable. +func TestSystemMetricsCollisionsFromFixture(t *testing.T) { + t.Parallel() + + systemMetrics, ok := loadDiagnosticData83Fixture(t)["systemMetrics"].(bson.M) + require.True(t, ok) + + labels := map[string]string{"cl_id": "", "cl_role": ""} + metricsByName := gatherMetrics(t, makeMetrics("systemMetrics", systemMetrics, labels, false)) + + collided, ok := metricsByName["mongodb_sys_ethtool_ens192_giant_hdr"] + require.True(t, ok) + assert.ElementsMatch(t, []string{" giant hdr", " giant hdr"}, + labelValues(collided, collisionLabel)) + + // Counters with a unique name keep their plain identity. + unique, ok := metricsByName["mongodb_sys_ethtool_ens192_ucast_pkts_tx"] + require.True(t, ok) + assert.Empty(t, labelValues(unique, collisionLabel)) +} + +// The whole captured reply has to survive a scrape, not only the ethtool subtree, and it is the +// only place where the 8.3 payload is walked with histograms enabled. +func TestDiagnosticData83Gathers(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + includeHistograms bool + }{ + {name: "without histograms"}, + {name: "with histograms", includeHistograms: true}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + labels := map[string]string{"cl_id": "", "cl_role": ""} + metricsByName := gatherMetrics(t, makeMetricsWithHistograms("", + loadDiagnosticData83Fixture(t), labels, false, tc.includeHistograms)) + + assert.Contains(t, metricsByName, "mongodb_sys_ethtool_ens192_giant_hdr") + + _, ok := metricsByName["mongodb_ss_metrics_query_cbr_histograms_micros_count"] + assert.Equal(t, tc.includeHistograms, ok, "histogram buckets follow the flag") + }) + } +} + +func TestCollidingFields(t *testing.T) { + t.Parallel() + + assert.Nil(t, collidingFields("systemMetrics.", bson.M{"a b": int64(1), "c d": int64(2)})) + assert.Nil(t, collidingFields("systemMetrics.", bson.M{"plain": int64(1)})) + assert.Nil(t, collidingFields("systemMetrics.", bson.M{"a_b": int64(1), "c_d": int64(2)})) + assert.Equal(t, map[string]string{"a b": "a b", "a_b": "a b"}, + collidingFields("systemMetrics.", bson.M{ + "a b": int64(1), + "a_b": int64(2), + "other": int64(3), + })) + + // prometheusize also drops a trailing underscore and collapses underscore runs, so fields + // differing only there end up sharing one metric name. + assert.Equal(t, map[string]string{"giant hdr": "giant hdr", "giant hdr#": "giant hdr"}, + collidingFields("systemMetrics.ethtool.ens192.", bson.M{ + "giant hdr": int64(1), + "giant hdr#": int64(2), + })) + assert.Equal(t, map[string]string{"a__b": "a__b", "a_b": "a__b"}, + collidingFields("systemMetrics.", bson.M{"a__b": int64(1), "a_b": int64(2)})) +} + +// Index names are user controlled and two of them can sanitize to the same string. They are +// exported as an index_name label value, so they never collide in the metric name and must not +// be given a collision label either: only a part of the family would carry it. +func TestFieldsExportedAsLabelValuesAreNotIndexed(t *testing.T) { + t.Parallel() + + metrics := makeMetrics("collstats.storageStats", bson.M{ + "indexSizes": bson.M{ + "a.b_1": int64(11), + "a_b_1": int64(22), + "c_1": int64(33), + }, + }, nil, false) + + sizes, ok := gatherMetrics(t, metrics)["mongodb_collstats_storageStats_indexSizes"] + require.True(t, ok) + assert.ElementsMatch(t, []string{"a.b_1", "a_b_1", "c_1"}, labelValues(sizes, "index_name")) + assert.Empty(t, labelValues(sizes, collisionLabel)) +} + +func TestIsUnambiguousKey(t *testing.T) { + t.Parallel() + + for _, k := range []string{"plain", "a_b", "Tx0_queue_1"} { + assert.True(t, isUnambiguousKey(k), k) + } + + // Everything prometheusize would rewrite has to be reported as ambiguous. + for _, k := range []string{"", "_", "a b", "a-b", "a__b", "_a", "a_", "Tx Queue#", "říká"} { + assert.False(t, isUnambiguousKey(k), k) + } +} + func TestAsMetricMapHandlesBSONM(t *testing.T) { t.Parallel() diff --git a/exporter/testdata/get_diagnostic_data_8.3.json b/exporter/testdata/get_diagnostic_data_8.3.json new file mode 100644 index 000000000..78a31f79c --- /dev/null +++ b/exporter/testdata/get_diagnostic_data_8.3.json @@ -0,0 +1,492 @@ +{ + "serverStatus": { + "metrics": { + "query": { + "allowDiskUseFalse": 0, + "deleteManyCount": 1, + "deleteOneNonTargetedShardedCount": 0, + "deleteOneTargetedShardedCount": 0, + "deleteOneUnshardedCount": 0, + "deleteOneWithoutShardKeyWithIdCount": 0, + "deleteOneWithoutShardKeyWithIdRetryCount": 0, + "externalRetryableWriteCount": 4, + "findAndModifyNonTargetedShardedCount": 0, + "findAndModifyTargetedShardedCount": 0, + "findAndModifyUnshardedCount": 0, + "internalRetryableWriteCount": 0, + "nonRetryableDeleteOneWithoutShardKeyWithIdCount": 0, + "nonRetryableUpdateOneWithoutShardKeyWithIdCount": 0, + "recordIdDeduplicationSwitchedToRoaring": 0, + "retryableInternalTransactionCount": 0, + "totalSlowQueryLogs": 24, + "updateDeleteManyDocumentsMaxCount": 1, + "updateDeleteManyDocumentsTotalCount": 1, + "updateDeleteManyDurationMaxMs": 2, + "updateDeleteManyDurationTotalMs": 2, + "updateManyCount": 0, + "updateOneNonTargetedShardedCount": 0, + "updateOneOpStyleBroadcastWithExactIDCount": 0, + "updateOneTargetedShardedCount": 0, + "updateOneUnshardedCount": 0, + "updateOneWithoutShardKeyWithIdCount": 0, + "updateOneWithoutShardKeyWithIdRetryCount": 0, + "bucketAuto": { + "spilledBytes": 0, + "spilledDataStorageSize": 0, + "spilledRecords": 0, + "spills": 0 + }, + "cbr": { + "choseWinningPlan": 0, + "count": 0, + "micros": 0, + "numPlans": 0, + "numPlansFailedCostEstimation": 0, + "numPlansTiedCostEstimation": 0, + "samplingMicros": 0, + "histograms": { + "micros": [ + { + "lowerBound": 0, + "count": 0 + }, + { + "lowerBound": 1024, + "count": 0 + }, + { + "lowerBound": 4096, + "count": 0 + }, + { + "lowerBound": 16384, + "count": 0 + }, + { + "lowerBound": 65536, + "count": 0 + }, + { + "lowerBound": 262144, + "count": 0 + }, + { + "lowerBound": 1048576, + "count": 0 + }, + { + "lowerBound": 4194304, + "count": 0 + }, + { + "lowerBound": 16777216, + "count": 0 + }, + { + "lowerBound": 67108864, + "count": 0 + }, + { + "lowerBound": 268435456, + "count": 0 + }, + { + "lowerBound": 1073741824, + "count": 0 + } + ], + "numPlans": [ + { + "lowerBound": 0, + "count": 0 + }, + { + "lowerBound": 2, + "count": 0 + }, + { + "lowerBound": 4, + "count": 0 + }, + { + "lowerBound": 8, + "count": 0 + }, + { + "lowerBound": 16, + "count": 0 + }, + { + "lowerBound": 32, + "count": 0 + } + ], + "samplingMicros": [ + { + "lowerBound": 0, + "count": 0 + }, + { + "lowerBound": 1024, + "count": 0 + }, + { + "lowerBound": 4096, + "count": 0 + }, + { + "lowerBound": 16384, + "count": 0 + }, + { + "lowerBound": 65536, + "count": 0 + }, + { + "lowerBound": 262144, + "count": 0 + }, + { + "lowerBound": 1048576, + "count": 0 + }, + { + "lowerBound": 4194304, + "count": 0 + }, + { + "lowerBound": 16777216, + "count": 0 + }, + { + "lowerBound": 67108864, + "count": 0 + }, + { + "lowerBound": 268435456, + "count": 0 + }, + { + "lowerBound": 1073741824, + "count": 0 + } + ] + } + }, + "expressionSimplifier": { + "abortedTooLarge": 0, + "notSimplified": 3265, + "simplified": 0, + "trivial": 2636 + }, + "geoNear": { + "spilledBytes": 0, + "spilledDataStorageSize": 0, + "spilledRecords": 0, + "spills": 0 + }, + "graphLookup": { + "spilledBytes": 0, + "spilledDataStorageSize": 0, + "spilledRecords": 0, + "spills": 0 + }, + "group": { + "spilledBytes": 0, + "spilledDataStorageSize": 0, + "spilledRecords": 0, + "spills": 0 + }, + "hashJoin": { + "spilledBytes": 0, + "spilledDataStorageSize": 0, + "spilledRecords": 0, + "spills": 0 + }, + "lookup": { + "dynamicIndexedLoopJoin": 0, + "hashLookup": 0, + "hashLookupSpillToDisk": 0, + "hashLookupSpillToDiskBytes": 0, + "hashLookupSpilledBytes": 0, + "hashLookupSpilledDataStorageSize": 0, + "hashLookupSpilledRecords": 0, + "hashLookupSpills": 0, + "indexedLoopJoin": 0, + "nestedLoopJoin": 0 + }, + "multiPlanner": { + "allPlansHitMemoryLimit": 0, + "choseWinningPlan": 0, + "classicCount": 0, + "classicMicros": 0, + "classicNumPlans": 0, + "classicWorks": 0, + "histograms": { + "classicMicros": [ + { + "lowerBound": 0, + "count": 0 + }, + { + "lowerBound": 1024, + "count": 0 + }, + { + "lowerBound": 4096, + "count": 0 + }, + { + "lowerBound": 16384, + "count": 0 + }, + { + "lowerBound": 65536, + "count": 0 + }, + { + "lowerBound": 262144, + "count": 0 + }, + { + "lowerBound": 1048576, + "count": 0 + }, + { + "lowerBound": 4194304, + "count": 0 + }, + { + "lowerBound": 16777216, + "count": 0 + }, + { + "lowerBound": 67108864, + "count": 0 + }, + { + "lowerBound": 268435456, + "count": 0 + }, + { + "lowerBound": 1073741824, + "count": 0 + } + ], + "classicNumPlans": [ + { + "lowerBound": 0, + "count": 0 + }, + { + "lowerBound": 2, + "count": 0 + }, + { + "lowerBound": 4, + "count": 0 + }, + { + "lowerBound": 8, + "count": 0 + }, + { + "lowerBound": 16, + "count": 0 + }, + { + "lowerBound": 32, + "count": 0 + } + ], + "classicWorks": [ + { + "lowerBound": 0, + "count": 0 + }, + { + "lowerBound": 128, + "count": 0 + }, + { + "lowerBound": 256, + "count": 0 + }, + { + "lowerBound": 512, + "count": 0 + }, + { + "lowerBound": 1024, + "count": 0 + }, + { + "lowerBound": 2048, + "count": 0 + }, + { + "lowerBound": 4096, + "count": 0 + }, + { + "lowerBound": 8192, + "count": 0 + }, + { + "lowerBound": 16384, + "count": 0 + }, + { + "lowerBound": 32768, + "count": 0 + } + ] + }, + "rateLimiter": { + "allowed": 0, + "delayed": 0, + "released": 0 + }, + "stoppingCondition": { + "hitEof": 0, + "hitResultsLimit": 0, + "hitWorksLimit": 0 + } + }, + "planCache": { + "totalQueryShapes": 0, + "totalSizeEstimateBytes": 0, + "classic": { + "cached_plans_evicted": 0, + "hits": 0, + "inactive_cached_plans_replaced": 0, + "misses": 481, + "replanned": 0, + "replanned_plan_is_cached_plan": 0, + "skipped": 2292 + }, + "sbe": { + "cached_plans_evicted": 0, + "hits": 0, + "inactive_cached_plans_replaced": 0, + "misses": 0, + "replanned": 0, + "replanned_plan_is_cached_plan": 0, + "skipped": 0 + } + }, + "planning": { + "invocationCount": 505, + "fastPath": { + "express": 2409, + "idHack": 72 + } + }, + "queryFramework": { + "aggregate": { + "classicHybrid": 7, + "classicOnly": 1439, + "sbeHybrid": 2, + "sbeOnly": 11 + }, + "find": { + "classic": 2699, + "sbe": 0 + } + }, + "recordIdDeduplication": { + "IXSCAN": { + "deduplicatedBytes": 0, + "deduplicatedRecords": 0 + }, + "OR": { + "deduplicatedBytes": 0, + "deduplicatedRecords": 0 + }, + "SORT_MERGE": { + "deduplicatedBytes": 0, + "deduplicatedRecords": 0 + }, + "unique": { + "deduplicatedBytes": 0, + "deduplicatedRecords": 0 + }, + "unique_roaring": { + "deduplicatedBytes": 0, + "deduplicatedRecords": 0 + } + }, + "setWindowFields": { + "spilledBytes": 0, + "spilledDataStorageSize": 0, + "spilledRecords": 0, + "spills": 0 + }, + "sort": { + "spillToDisk": 0, + "spillToDiskBytes": 0, + "totalBytesSorted": 17274146, + "totalKeysSorted": 3440 + }, + "subPlanner": { + "classicChoseWinningPlan": 0 + }, + "textOr": { + "spilledBytes": 0, + "spilledDataStorageSize": 0, + "spilledRecords": 0, + "spills": 0 + } + } + } + }, + "systemMetrics": { + "ethtool": { + "ens192": { + "Tx Queue#": 0, + " TSO pkts tx": 0, + " TSO bytes tx": -102, + " ucast pkts tx": -102, + " ucast bytes tx": 78, + " mcast pkts tx": 0, + " mcast bytes tx": 0, + " bcast pkts tx": 0, + " bcast bytes tx": 0, + " pkts tx err": 0, + " pkts tx discard": -10, + " drv dropped tx total": -59, + " too many frags": -89, + " giant hdr": 104, + " hdr err": 0, + " tso": 0, + " ring full": 0, + " pkts linearized": 0, + " hdr cloned": 0, + " giant hdr": 0, + " xdp xmit": 0, + " xdp xmit err": 0, + "Rx Queue#": -113, + " LRO pkts rx": 4, + " LRO byte rx": 0, + " ucast pkts rx": 0, + " ucast bytes rx": 0, + " mcast pkts rx": 0, + " mcast bytes rx": 0, + " bcast pkts rx": 0, + " bcast bytes rx": 0, + " pkts rx OOB": 0, + " pkts rx err": 0, + " drv dropped rx total": 0, + " err": 0, + " fcs": 0, + " rx buf alloc fail": 0, + " xdp packets": 0, + " xdp tx": 0, + " xdp redirects": 0, + " xdp drops": 0, + " xdp aborted": 0, + "tx timeout count": 0 + } + } + } +}