diff --git a/examples/exemplars/main.go b/examples/exemplars/main.go index 618224a7b..f7b7ac0e6 100644 --- a/examples/exemplars/main.go +++ b/examples/exemplars/main.go @@ -32,6 +32,7 @@ func main() { requestDurations := prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "A histogram of the HTTP request durations in seconds.", + Unit: "seconds", Buckets: prometheus.ExponentialBuckets(0.1, 1.5, 5), }) diff --git a/prometheus/counter.go b/prometheus/counter.go index 4ce84e7a8..094be07a6 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -90,6 +90,7 @@ func NewCounter(opts CounterOpts) Counter { opts.Help, nil, opts.ConstLabels, + opts.Unit, ) if opts.now == nil { opts.now = time.Now @@ -203,6 +204,7 @@ func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec { desc := V2.NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, + // opts.Unit, // QUESTION: shall I put this for last? opts.VariableLabels, opts.ConstLabels, ) @@ -354,5 +356,6 @@ func NewCounterFunc(opts CounterOpts, function func() float64) CounterFunc { opts.Help, nil, opts.ConstLabels, + opts.Unit, ), CounterValue, function) } diff --git a/prometheus/desc.go b/prometheus/desc.go index 46dd59ac5..0d7aa2ae4 100644 --- a/prometheus/desc.go +++ b/prometheus/desc.go @@ -47,6 +47,8 @@ type Desc struct { fqName string // help provides some helpful information about this metric. help string + // unit provides the unit of this metric. + unit string // constLabelPairs contains precalculated DTO label pairs based on // the constant labels. constLabelPairs []*dto.LabelPair @@ -66,6 +68,13 @@ type Desc struct { err error } +func optionalUnitValue(unit ...string) string { + if len(unit) > 0 { + return unit[0] + } + return "" +} + // NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc // and will be reported on registration time. variableLabels and constLabels can // be nil if no such labels should be set. fqName must not be empty. @@ -75,8 +84,8 @@ type Desc struct { // // For constLabels, the label values are constant. Therefore, they are fully // specified in the Desc. See the Collector example for a usage pattern. -func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc { - return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels) +func NewDesc(fqName, help string, variableLabels []string, constLabels Labels, unit ...string) *Desc { + return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels, optionalUnitValue(unit...)) } // NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc @@ -89,11 +98,12 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) * // // For constLabels, the label values are constant. Therefore, they are fully // specified in the Desc. See the Collector example for a usage pattern. -func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc { +func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, unit ...string) *Desc { d := &Desc{ fqName: fqName, help: help, variableLabels: variableLabels.compile(), + unit: optionalUnitValue(unit...), } //nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme. if !model.NameValidationScheme.IsValidMetricName(fqName) { @@ -150,10 +160,11 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const d.id = xxh.Sum64() // Sort labelNames so that order doesn't matter for the hash. sort.Strings(labelNames) - // Now hash together (in this order) the help string and the sorted + // Now hash together (in this order) the help string, the unit string and the sorted // label names. xxh.Reset() xxh.WriteString(help) + xxh.WriteString(optionalUnitValue(unit...)) xxh.Write(separatorByteSlice) for _, labelName := range labelNames { xxh.WriteString(labelName) @@ -211,9 +222,10 @@ func (d *Desc) String() string { } } return fmt.Sprintf( - "Desc{fqName: %q, help: %q, constLabels: {%s}, variableLabels: {%s}}", + "Desc{fqName: %q, help: %q, unit: %q, constLabels: {%s}, variableLabels: {%s}}", d.fqName, d.help, + d.unit, strings.Join(lpStrings, ","), strings.Join(vlStrings, ","), ) diff --git a/prometheus/desc_test.go b/prometheus/desc_test.go index 0570d5bd1..e86995fd6 100644 --- a/prometheus/desc_test.go +++ b/prometheus/desc_test.go @@ -62,7 +62,7 @@ func TestNewDescWithNilLabelValues_String(t *testing.T) { nil, nil, ) - if desc.String() != `Desc{fqName: "sample_label", help: "sample label", constLabels: {}, variableLabels: {}}` { + if desc.String() != `Desc{fqName: "sample_label", help: "sample label", unit: "", constLabels: {}, variableLabels: {}}` { t.Errorf("String: unexpected output: %s", desc.String()) } } @@ -71,7 +71,20 @@ func TestNewInvalidDesc_String(t *testing.T) { desc := NewInvalidDesc( nil, ) - if desc.String() != `Desc{fqName: "", help: "", constLabels: {}, variableLabels: {}}` { + if desc.String() != `Desc{fqName: "", help: "", unit: "", constLabels: {}, variableLabels: {}}` { t.Errorf("String: unexpected output: %s", desc.String()) } } + +func TestNewDescWithUnit_String(t *testing.T) { + desc := NewDesc( + "sample_metric_bytes", + "sample metric with unit", + nil, + nil, + "bytes", + ) + if desc.String() != `Desc{fqName: "sample_metric_bytes", help: "sample metric with unit", unit: "bytes", constLabels: {}, variableLabels: {}}` { + t.Errorf("String: unexpected output:\ngot: %s\nwant: %s", desc.String(), desc.String()) + } +} diff --git a/prometheus/example_metricvec_test.go b/prometheus/example_metricvec_test.go index 59e43f8f8..0a6234364 100644 --- a/prometheus/example_metricvec_test.go +++ b/prometheus/example_metricvec_test.go @@ -52,8 +52,12 @@ type InfoVec struct { *prometheus.MetricVec } -func NewInfoVec(name, help string, labelNames []string) *InfoVec { - desc := prometheus.NewDesc(name, help, labelNames, nil) +func NewInfoVec(name, help string, labelNames []string, unit ...string) *InfoVec { + var u string + if len(unit) > 0 { + u = unit[0] + } + desc := prometheus.NewDesc(name, help, labelNames, nil, u) return &InfoVec{ MetricVec: prometheus.NewMetricVec(desc, func(lvs ...string) prometheus.Metric { if len(lvs) != len(labelNames) { diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index 88164ccc5..7c137dbe2 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -310,9 +310,9 @@ func ExampleRegister() { // Output: // taskCounter registered. - // taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: {worker_id}} has different label names or a different help string + // taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", unit: "", constLabels: {}, variableLabels: {worker_id}} has different label names or a different help string // taskCounter unregistered. - // taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: {worker_id}} has different label names or a different help string + // taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", unit: "", constLabels: {}, variableLabels: {worker_id}} has different label names or a different help string // taskCounterVec registered. // Worker initialization failed: inconsistent label cardinality: expected 1 label values but got 2 in []string{"42", "spurious arg"} // notMyCounter is nil. @@ -386,6 +386,7 @@ func ExampleNewConstSummary() { "A summary of the HTTP request durations.", []string{"code", "method"}, prometheus.Labels{"owner": "example"}, + "seconds", ) // Create a constant summary from values we got from a 3rd party telemetry system. @@ -440,7 +441,8 @@ func ExampleHistogram() { temps := prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "pond_temperature_celsius", Help: "The temperature of the frog pond.", // Sorry, we can't measure how badly it smells. - Buckets: prometheus.LinearBuckets(20, 5, 5), // 5 buckets, each 5 centigrade wide. + Unit: "celsius", + Buckets: prometheus.LinearBuckets(20, 5, 5), // 5 buckets, each 5 centigrade wide. }) // Simulate some observations. @@ -466,6 +468,7 @@ func ExampleNewConstHistogram() { "A histogram of the HTTP request durations.", []string{"code", "method"}, prometheus.Labels{"owner": "example"}, + "seconds", ) // Create a constant histogram from values we got from a 3rd party telemetry system. @@ -521,6 +524,7 @@ func ExampleNewConstHistogram_withExemplar() { "A histogram of the HTTP request durations.", []string{"code", "method"}, prometheus.Labels{"owner": "example"}, + "seconds", ) // Create a constant histogram from values we got from a 3rd party telemetry system. @@ -690,6 +694,7 @@ func ExampleNewMetricWithTimestamp() { "temperature_kelvin", "Current temperature in Kelvin.", nil, nil, + "kelvin", ) // Create a constant gauge from values we got from an external @@ -723,6 +728,7 @@ func ExampleNewConstMetricWithCreatedTimestamp() { "time_since_epoch_seconds", "Current epoch time in seconds.", nil, nil, + "seconds", ) timeSinceEpochReportedByExternalSystem := time.Date(2009, time.November, 10, 23, 0, 0, 12345678, time.UTC) diff --git a/prometheus/gauge.go b/prometheus/gauge.go index dd2eac940..969359679 100644 --- a/prometheus/gauge.go +++ b/prometheus/gauge.go @@ -81,6 +81,7 @@ func NewGauge(opts GaugeOpts) Gauge { opts.Help, nil, opts.ConstLabels, + opts.Unit, ) result := &gauge{desc: desc, labelPairs: desc.constLabelPairs} result.init(result) // Init self-collection. @@ -163,6 +164,7 @@ func (v2) NewGaugeVec(opts GaugeVecOpts) *GaugeVec { opts.Help, opts.VariableLabels, opts.ConstLabels, + opts.Unit, ) return &GaugeVec{ MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { @@ -307,5 +309,6 @@ func NewGaugeFunc(opts GaugeOpts, function func() float64) GaugeFunc { opts.Help, nil, opts.ConstLabels, + opts.Unit, ), GaugeValue, function) } diff --git a/prometheus/gauge_test.go b/prometheus/gauge_test.go index 26759fbbc..3e2285b7c 100644 --- a/prometheus/gauge_test.go +++ b/prometheus/gauge_test.go @@ -171,7 +171,7 @@ func TestGaugeFunc(t *testing.T) { func() float64 { return 3.1415 }, ) - if expected, got := `Desc{fqName: "test_name", help: "test help", constLabels: {a="1",b="2"}, variableLabels: {}}`, gf.Desc().String(); expected != got { + if expected, got := `Desc{fqName: "test_name", help: "test help", unit: "", constLabels: {a="1",b="2"}, variableLabels: {}}`, gf.Desc().String(); expected != got { t.Errorf("expected %q, got %q", expected, got) } diff --git a/prometheus/go_collector_latest.go b/prometheus/go_collector_latest.go index 6b8684731..bdbbf7f55 100644 --- a/prometheus/go_collector_latest.go +++ b/prometheus/go_collector_latest.go @@ -98,7 +98,7 @@ type goCollector struct { // snapshot is always produced by Collect. mu sync.Mutex - // Contains all samples that has to retrieved from runtime/metrics (not all of them will be exposed). + // Contains all samples that have to be retrieved from runtime/metrics (not all of them will be exposed). sampleBuf []metrics.Sample // sampleMap allows lookup for MemStats metrics and runtime/metrics histograms for exact sums. sampleMap map[string]*metrics.Sample @@ -210,16 +210,19 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector { sampleBuf = append(sampleBuf, metrics.Sample{Name: d.Name}) sampleMap[d.Name] = &sampleBuf[len(sampleBuf)-1] + // Extract unit from the runtime/metrics name (e.g., "/gc/heap/allocs:bytes" -> "bytes") + unit := d.Name[strings.IndexRune(d.Name, ':')+1:] + var m collectorMetric if d.Kind == metrics.KindFloat64Histogram { _, hasSum := opt.RuntimeMetricSumForHist[d.Name] - unit := d.Name[strings.IndexRune(d.Name, ':')+1:] m = newBatchHistogram( NewDesc( BuildFQName(namespace, subsystem, name), help, nil, nil, + unit, ), internal.RuntimeMetricsBucketsForUnit(bucketsMap[d.Name], unit), hasSum, @@ -230,6 +233,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector { Subsystem: subsystem, Name: name, Help: help, + Unit: unit, }, ) } else { @@ -238,6 +242,7 @@ func NewGoCollector(opts ...func(o *internal.GoCollectorOptions)) Collector { Subsystem: subsystem, Name: name, Help: help, + Unit: unit, }) } metricSet = append(metricSet, m) diff --git a/prometheus/histogram.go b/prometheus/histogram.go index c453b754a..1f0f92892 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -378,6 +378,9 @@ type HistogramOpts struct { // string. Help string + // Unit provides the unit of this Histogram. + Unit string + // ConstLabels are used to attach fixed labels to this metric. Metrics // with the same fully-qualified name must have the same label names in // their ConstLabels. @@ -527,6 +530,7 @@ func NewHistogram(opts HistogramOpts) Histogram { opts.Help, nil, opts.ConstLabels, + opts.Unit, ), opts, ) @@ -1190,6 +1194,7 @@ func (v2) NewHistogramVec(opts HistogramVecOpts) *HistogramVec { opts.Help, opts.VariableLabels, opts.ConstLabels, + opts.Unit, ) return &HistogramVec{ MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { diff --git a/prometheus/metric.go b/prometheus/metric.go index 76e59f128..c5cb90adf 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -81,6 +81,9 @@ type Opts struct { // string. Help string + // Unit provides the unit of this metric as per https://prometheus.io/docs/specs/om + Unit string + // ConstLabels are used to attach fixed labels to this metric. Metrics // with the same fully-qualified name must have the same label names in // their ConstLabels. diff --git a/prometheus/metric_test.go b/prometheus/metric_test.go index f6553c332..96960fee7 100644 --- a/prometheus/metric_test.go +++ b/prometheus/metric_test.go @@ -49,7 +49,7 @@ func TestWithExemplarsMetric(t *testing.T) { t.Run("histogram", func(t *testing.T) { // Create a constant histogram from values we got from a 3rd party telemetry system. h := MustNewConstHistogram( - NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil), + NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil, "seconds"), 4711, 403.34, // Four buckets, but we expect five as the +Inf bucket will be created if we see value outside of those buckets. map[float64]uint64{25: 121, 50: 2403, 100: 3221, 200: 4233}, @@ -99,7 +99,7 @@ func TestWithExemplarsNativeHistogramMetric(t *testing.T) { t.Run("native histogram single exemplar", func(t *testing.T) { // Create a constant histogram from values we got from a 3rd party telemetry system. h := MustNewConstNativeHistogram( - NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil), + NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil, "seconds"), 10, 12.1, map[int]int64{1: 7, 2: 1, 3: 2}, map[int]int64{}, 0, 2, 0.2, time.Date( 2009, 11, 17, 20, 34, 58, 651387237, time.UTC)) m := &withExemplarsMetric{Metric: h, exemplars: []*dto.Exemplar{ diff --git a/prometheus/promhttp/http.go b/prometheus/promhttp/http.go index e68160194..18e9d5bf5 100644 --- a/prometheus/promhttp/http.go +++ b/prometheus/promhttp/http.go @@ -226,12 +226,17 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO rsp.Header().Set(contentEncodingHeader, encodingHeader) } - var enc expfmt.Encoder + var ( + enc expfmt.Encoder + encOpts []expfmt.EncoderOption + ) if opts.EnableOpenMetricsTextCreatedSamples { - enc = expfmt.NewEncoder(w, contentType, expfmt.WithCreatedLines()) - } else { - enc = expfmt.NewEncoder(w, contentType) + encOpts = append(encOpts, expfmt.WithCreatedLines()) + } + if opts.EnableOpenMetricsUnit { + encOpts = append(encOpts, expfmt.WithUnit()) } + enc = expfmt.NewEncoder(w, contentType, encOpts...) // handleError handles the error according to opts.ErrorHandling // and returns true if we have to abort after the handling. @@ -461,6 +466,9 @@ type HandlerOpts struct { // Prometheus introduced the feature flag 'created-timestamp-zero-ingestion' // in version 2.50.0 to handle this situation. EnableOpenMetricsTextCreatedSamples bool + // EnableOpenMetricsUnit enables unit metadata in the OpenMetrics output format. + // This is only applicable when OpenMetrics format is negotiated. + EnableOpenMetricsUnit bool // ProcessStartTime allows setting process start timevalue that will be exposed // with "Process-Start-Time-Unix" response header along with the metrics // payload. This allow callers to have efficient transformations to cumulative diff --git a/prometheus/promhttp/http_test.go b/prometheus/promhttp/http_test.go index 60bed4242..58ed1a24b 100644 --- a/prometheus/promhttp/http_test.go +++ b/prometheus/promhttp/http_test.go @@ -159,11 +159,11 @@ func TestHandlerErrorHandling(t *testing.T) { t.Fatalf("unexpected number of done invokes, want 0, got %d", got) } - wantMsg := `error gathering metrics: error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: {}}: collect error + wantMsg := `error gathering metrics: error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", unit: "", constLabels: {}, variableLabels: {}}: collect error ` wantErrorBody := `An error has occurred while serving metrics: -error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", constLabels: {}, variableLabels: {}}: collect error +error collecting metric Desc{fqName: "invalid_metric", help: "not helpful", unit: "", constLabels: {}, variableLabels: {}}: collect error ` wantOKBody1 := `# HELP name docstring # TYPE name counter @@ -549,6 +549,40 @@ func TestNegotiateEncodingWriter(t *testing.T) { } } +func TestHandlerWithUnit(t *testing.T) { + reg := prometheus.NewRegistry() + + counter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "http_request_duration_seconds_total", + Help: "Total time spent handling HTTP requests.", + Unit: "seconds", + }) + reg.MustRegister(counter) + counter.Add(42) + + expectedOpenMetricsOutput := `# HELP http_request_duration_seconds Total time spent handling HTTP requests. +# TYPE http_request_duration_seconds counter +# UNIT http_request_duration_seconds seconds +http_request_duration_seconds_total 42.0 +# EOF +` + + handler := HandlerFor(reg, HandlerOpts{EnableOpenMetrics: true, EnableOpenMetricsUnit: true}) + writer := httptest.NewRecorder() + request, _ := http.NewRequest(http.MethodGet, "/", nil) + request.Header.Add(acceptHeader, "application/openmetrics-text") + + handler.ServeHTTP(writer, request) + + if got, want := writer.Header().Get(contentTypeHeader), "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=underscores"; got != want { + t.Errorf("expected Content-Type %q, got %q", want, got) + } + + if got := writer.Body.String(); got != expectedOpenMetricsOutput { + t.Errorf("expected body:\n%s\ngot:\n%s", expectedOpenMetricsOutput, got) + } +} + func BenchmarkCompression(b *testing.B) { benchmarks := []struct { name string diff --git a/prometheus/registry.go b/prometheus/registry.go index c6fd2f58b..2cdcdff6a 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -685,6 +685,9 @@ func processMetric( metricFamily = &dto.MetricFamily{} metricFamily.Name = proto.String(desc.fqName) metricFamily.Help = proto.String(desc.help) + if desc.unit != "" { + metricFamily.Unit = proto.String(desc.unit) + } // TODO(beorn7): Simplify switch once Desc has type. switch { case dtoMetric.Gauge != nil: diff --git a/prometheus/summary.go b/prometheus/summary.go index ac5203c6f..b8becf120 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -101,6 +101,9 @@ type SummaryOpts struct { // string. Help string + // Unit provides the unit of this Summary. + Unit string + // ConstLabels are used to attach fixed labels to this metric. Metrics // with the same fully-qualified name must have the same label names in // their ConstLabels. @@ -186,6 +189,7 @@ func NewSummary(opts SummaryOpts) Summary { opts.Help, nil, opts.ConstLabels, + opts.Unit, ), opts, ) @@ -578,6 +582,7 @@ func (v2) NewSummaryVec(opts SummaryVecOpts) *SummaryVec { opts.Help, opts.VariableLabels, opts.ConstLabels, + opts.Unit, ) return &SummaryVec{ MetricVec: NewMetricVec(desc, func(lvs ...string) Metric { diff --git a/prometheus/untyped.go b/prometheus/untyped.go index 0f9ce63f4..d43db964d 100644 --- a/prometheus/untyped.go +++ b/prometheus/untyped.go @@ -38,5 +38,6 @@ func NewUntypedFunc(opts UntypedOpts, function func() float64) UntypedFunc { opts.Help, nil, opts.ConstLabels, + opts.Unit, ), UntypedValue, function) } diff --git a/prometheus/wrap.go b/prometheus/wrap.go index 2ed128506..6ce58aea3 100644 --- a/prometheus/wrap.go +++ b/prometheus/wrap.go @@ -230,6 +230,7 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc { return &Desc{ fqName: desc.fqName, help: desc.help, + unit: desc.unit, variableLabels: desc.variableLabels, constLabelPairs: desc.constLabelPairs, err: fmt.Errorf("attempted wrapping with already existing label name %q", ln), @@ -238,7 +239,7 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc { constLabels[ln] = lv } // NewDesc will do remaining validations. - newDesc := V2.NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels) + newDesc := V2.NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels, desc.unit) // Propagate errors if there was any. This will override any errer // created by NewDesc above, i.e. earlier errors get precedence. if desc.err != nil { diff --git a/tutorials/whatsup/reference/main.go b/tutorials/whatsup/reference/main.go index d7ac1c7b7..9e623d9f5 100644 --- a/tutorials/whatsup/reference/main.go +++ b/tutorials/whatsup/reference/main.go @@ -93,7 +93,7 @@ func runMain(opts internal.Config) (err error) { Help: "Build information.", ConstLabels: map[string]string{ "version": "vYOLO", - "language": "Go 1.20", + "language": "Go 1.21", "owner": "@me", }, }, func() float64 {