go.mod: bump client-go and api machinerie

I had to run `make generate`.
Some API functions got additional parameters `Options` and `Context`.
I used empty options and `context.TODO()` for now.

Signed-off-by: leonnicolas <leonloechner@gmx.de>
This commit is contained in:
leonnicolas
2021-05-15 12:08:31 +02:00
parent f2c37b9de6
commit a3bf13711c
2386 changed files with 419055 additions and 183398 deletions

View File

@@ -30,17 +30,38 @@ type Encoder interface {
Encode(*dto.MetricFamily) error
}
type encoder func(*dto.MetricFamily) error
func (e encoder) Encode(v *dto.MetricFamily) error {
return e(v)
// Closer is implemented by Encoders that need to be closed to finalize
// encoding. (For example, OpenMetrics needs a final `# EOF` line.)
//
// Note that all Encoder implementations returned from this package implement
// Closer, too, even if the Close call is a no-op. This happens in preparation
// for adding a Close method to the Encoder interface directly in a (mildly
// breaking) release in the future.
type Closer interface {
Close() error
}
// Negotiate returns the Content-Type based on the given Accept header.
// If no appropriate accepted type is found, FmtText is returned.
type encoderCloser struct {
encode func(*dto.MetricFamily) error
close func() error
}
func (ec encoderCloser) Encode(v *dto.MetricFamily) error {
return ec.encode(v)
}
func (ec encoderCloser) Close() error {
return ec.close()
}
// Negotiate returns the Content-Type based on the given Accept header. If no
// appropriate accepted type is found, FmtText is returned (which is the
// Prometheus text format). This function will never negotiate FmtOpenMetrics,
// as the support is still experimental. To include the option to negotiate
// FmtOpenMetrics, use NegotiateOpenMetrics.
func Negotiate(h http.Header) Format {
for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
// Check for protocol buffer
ver := ac.Params["version"]
if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
switch ac.Params["encoding"] {
case "delimited":
@@ -51,8 +72,6 @@ func Negotiate(h http.Header) Format {
return FmtProtoCompact
}
}
// Check for text format.
ver := ac.Params["version"]
if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
return FmtText
}
@@ -60,29 +79,84 @@ func Negotiate(h http.Header) Format {
return FmtText
}
// NewEncoder returns a new encoder based on content type negotiation.
// NegotiateIncludingOpenMetrics works like Negotiate but includes
// FmtOpenMetrics as an option for the result. Note that this function is
// temporary and will disappear once FmtOpenMetrics is fully supported and as
// such may be negotiated by the normal Negotiate function.
func NegotiateIncludingOpenMetrics(h http.Header) Format {
for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
ver := ac.Params["version"]
if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
switch ac.Params["encoding"] {
case "delimited":
return FmtProtoDelim
case "text":
return FmtProtoText
case "compact-text":
return FmtProtoCompact
}
}
if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
return FmtText
}
if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion || ver == "") {
return FmtOpenMetrics
}
}
return FmtText
}
// NewEncoder returns a new encoder based on content type negotiation. All
// Encoder implementations returned by NewEncoder also implement Closer, and
// callers should always call the Close method. It is currently only required
// for FmtOpenMetrics, but a future (breaking) release will add the Close method
// to the Encoder interface directly. The current version of the Encoder
// interface is kept for backwards compatibility.
func NewEncoder(w io.Writer, format Format) Encoder {
switch format {
case FmtProtoDelim:
return encoder(func(v *dto.MetricFamily) error {
_, err := pbutil.WriteDelimited(w, v)
return err
})
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := pbutil.WriteDelimited(w, v)
return err
},
close: func() error { return nil },
}
case FmtProtoCompact:
return encoder(func(v *dto.MetricFamily) error {
_, err := fmt.Fprintln(w, v.String())
return err
})
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := fmt.Fprintln(w, v.String())
return err
},
close: func() error { return nil },
}
case FmtProtoText:
return encoder(func(v *dto.MetricFamily) error {
_, err := fmt.Fprintln(w, proto.MarshalTextString(v))
return err
})
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := fmt.Fprintln(w, proto.MarshalTextString(v))
return err
},
close: func() error { return nil },
}
case FmtText:
return encoder(func(v *dto.MetricFamily) error {
_, err := MetricFamilyToText(w, v)
return err
})
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := MetricFamilyToText(w, v)
return err
},
close: func() error { return nil },
}
case FmtOpenMetrics:
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := MetricFamilyToOpenMetrics(w, v)
return err
},
close: func() error {
_, err := FinalizeOpenMetrics(w)
return err
},
}
}
panic("expfmt.NewEncoder: unknown format")
panic(fmt.Errorf("expfmt.NewEncoder: unknown format %q", format))
}

View File

@@ -19,10 +19,12 @@ type Format string
// Constants to assemble the Content-Type values for the different wire protocols.
const (
TextVersion = "0.0.4"
ProtoType = `application/vnd.google.protobuf`
ProtoProtocol = `io.prometheus.client.MetricFamily`
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"
TextVersion = "0.0.4"
ProtoType = `application/vnd.google.protobuf`
ProtoProtocol = `io.prometheus.client.MetricFamily`
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"
OpenMetricsType = `application/openmetrics-text`
OpenMetricsVersion = "0.0.1"
// The Content-Type values for the different wire protocols.
FmtUnknown Format = `<unknown>`
@@ -30,6 +32,7 @@ const (
FmtProtoDelim Format = ProtoFmt + ` encoding=delimited`
FmtProtoText Format = ProtoFmt + ` encoding=text`
FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text`
FmtOpenMetrics Format = OpenMetricsType + `; version=` + OpenMetricsVersion + `; charset=utf-8`
)
const (

View File

@@ -0,0 +1,527 @@
// Copyright 2020 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package expfmt
import (
"bufio"
"bytes"
"fmt"
"io"
"math"
"strconv"
"strings"
"github.com/golang/protobuf/ptypes"
"github.com/prometheus/common/model"
dto "github.com/prometheus/client_model/go"
)
// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the
// OpenMetrics text format and writes the resulting lines to 'out'. It returns
// the number of bytes written and any error encountered. The output will have
// the same order as the input, no further sorting is performed. Furthermore,
// this function assumes the input is already sanitized and does not perform any
// sanity checks. If the input contains duplicate metrics or invalid metric or
// label names, the conversion will result in invalid text format output.
//
// This function fulfills the type 'expfmt.encoder'.
//
// Note that OpenMetrics requires a final `# EOF` line. Since this function acts
// on individual metric families, it is the responsibility of the caller to
// append this line to 'out' once all metric families have been written.
// Conveniently, this can be done by calling FinalizeOpenMetrics.
//
// The output should be fully OpenMetrics compliant. However, there are a few
// missing features and peculiarities to avoid complications when switching from
// Prometheus to OpenMetrics or vice versa:
//
// - Counters are expected to have the `_total` suffix in their metric name. In
// the output, the suffix will be truncated from the `# TYPE` and `# HELP`
// line. A counter with a missing `_total` suffix is not an error. However,
// its type will be set to `unknown` in that case to avoid invalid OpenMetrics
// output.
//
// - No support for the following (optional) features: `# UNIT` line, `_created`
// line, info type, stateset type, gaugehistogram type.
//
// - The size of exemplar labels is not checked (i.e. it's possible to create
// exemplars that are larger than allowed by the OpenMetrics specification).
//
// - The value of Counters is not checked. (OpenMetrics doesn't allow counters
// with a `NaN` value.)
func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int, err error) {
name := in.GetName()
if name == "" {
return 0, fmt.Errorf("MetricFamily has no name: %s", in)
}
// Try the interface upgrade. If it doesn't work, we'll use a
// bufio.Writer from the sync.Pool.
w, ok := out.(enhancedWriter)
if !ok {
b := bufPool.Get().(*bufio.Writer)
b.Reset(out)
w = b
defer func() {
bErr := b.Flush()
if err == nil {
err = bErr
}
bufPool.Put(b)
}()
}
var (
n int
metricType = in.GetType()
shortName = name
)
if metricType == dto.MetricType_COUNTER && strings.HasSuffix(shortName, "_total") {
shortName = name[:len(name)-6]
}
// Comments, first HELP, then TYPE.
if in.Help != nil {
n, err = w.WriteString("# HELP ")
written += n
if err != nil {
return
}
n, err = w.WriteString(shortName)
written += n
if err != nil {
return
}
err = w.WriteByte(' ')
written++
if err != nil {
return
}
n, err = writeEscapedString(w, *in.Help, true)
written += n
if err != nil {
return
}
err = w.WriteByte('\n')
written++
if err != nil {
return
}
}
n, err = w.WriteString("# TYPE ")
written += n
if err != nil {
return
}
n, err = w.WriteString(shortName)
written += n
if err != nil {
return
}
switch metricType {
case dto.MetricType_COUNTER:
if strings.HasSuffix(name, "_total") {
n, err = w.WriteString(" counter\n")
} else {
n, err = w.WriteString(" unknown\n")
}
case dto.MetricType_GAUGE:
n, err = w.WriteString(" gauge\n")
case dto.MetricType_SUMMARY:
n, err = w.WriteString(" summary\n")
case dto.MetricType_UNTYPED:
n, err = w.WriteString(" unknown\n")
case dto.MetricType_HISTOGRAM:
n, err = w.WriteString(" histogram\n")
default:
return written, fmt.Errorf("unknown metric type %s", metricType.String())
}
written += n
if err != nil {
return
}
// Finally the samples, one line for each.
for _, metric := range in.Metric {
switch metricType {
case dto.MetricType_COUNTER:
if metric.Counter == nil {
return written, fmt.Errorf(
"expected counter in metric %s %s", name, metric,
)
}
// Note that we have ensured above that either the name
// ends on `_total` or that the rendered type is
// `unknown`. Therefore, no `_total` must be added here.
n, err = writeOpenMetricsSample(
w, name, "", metric, "", 0,
metric.Counter.GetValue(), 0, false,
metric.Counter.Exemplar,
)
case dto.MetricType_GAUGE:
if metric.Gauge == nil {
return written, fmt.Errorf(
"expected gauge in metric %s %s", name, metric,
)
}
n, err = writeOpenMetricsSample(
w, name, "", metric, "", 0,
metric.Gauge.GetValue(), 0, false,
nil,
)
case dto.MetricType_UNTYPED:
if metric.Untyped == nil {
return written, fmt.Errorf(
"expected untyped in metric %s %s", name, metric,
)
}
n, err = writeOpenMetricsSample(
w, name, "", metric, "", 0,
metric.Untyped.GetValue(), 0, false,
nil,
)
case dto.MetricType_SUMMARY:
if metric.Summary == nil {
return written, fmt.Errorf(
"expected summary in metric %s %s", name, metric,
)
}
for _, q := range metric.Summary.Quantile {
n, err = writeOpenMetricsSample(
w, name, "", metric,
model.QuantileLabel, q.GetQuantile(),
q.GetValue(), 0, false,
nil,
)
written += n
if err != nil {
return
}
}
n, err = writeOpenMetricsSample(
w, name, "_sum", metric, "", 0,
metric.Summary.GetSampleSum(), 0, false,
nil,
)
written += n
if err != nil {
return
}
n, err = writeOpenMetricsSample(
w, name, "_count", metric, "", 0,
0, metric.Summary.GetSampleCount(), true,
nil,
)
case dto.MetricType_HISTOGRAM:
if metric.Histogram == nil {
return written, fmt.Errorf(
"expected histogram in metric %s %s", name, metric,
)
}
infSeen := false
for _, b := range metric.Histogram.Bucket {
n, err = writeOpenMetricsSample(
w, name, "_bucket", metric,
model.BucketLabel, b.GetUpperBound(),
0, b.GetCumulativeCount(), true,
b.Exemplar,
)
written += n
if err != nil {
return
}
if math.IsInf(b.GetUpperBound(), +1) {
infSeen = true
}
}
if !infSeen {
n, err = writeOpenMetricsSample(
w, name, "_bucket", metric,
model.BucketLabel, math.Inf(+1),
0, metric.Histogram.GetSampleCount(), true,
nil,
)
written += n
if err != nil {
return
}
}
n, err = writeOpenMetricsSample(
w, name, "_sum", metric, "", 0,
metric.Histogram.GetSampleSum(), 0, false,
nil,
)
written += n
if err != nil {
return
}
n, err = writeOpenMetricsSample(
w, name, "_count", metric, "", 0,
0, metric.Histogram.GetSampleCount(), true,
nil,
)
default:
return written, fmt.Errorf(
"unexpected type in metric %s %s", name, metric,
)
}
written += n
if err != nil {
return
}
}
return
}
// FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics.
func FinalizeOpenMetrics(w io.Writer) (written int, err error) {
return w.Write([]byte("# EOF\n"))
}
// writeOpenMetricsSample writes a single sample in OpenMetrics text format to
// w, given the metric name, the metric proto message itself, optionally an
// additional label name with a float64 value (use empty string as label name if
// not required), the value (optionally as float64 or uint64, determined by
// useIntValue), and optionally an exemplar (use nil if not required). The
// function returns the number of bytes written and any error encountered.
func writeOpenMetricsSample(
w enhancedWriter,
name, suffix string,
metric *dto.Metric,
additionalLabelName string, additionalLabelValue float64,
floatValue float64, intValue uint64, useIntValue bool,
exemplar *dto.Exemplar,
) (int, error) {
var written int
n, err := w.WriteString(name)
written += n
if err != nil {
return written, err
}
if suffix != "" {
n, err = w.WriteString(suffix)
written += n
if err != nil {
return written, err
}
}
n, err = writeOpenMetricsLabelPairs(
w, metric.Label, additionalLabelName, additionalLabelValue,
)
written += n
if err != nil {
return written, err
}
err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
if useIntValue {
n, err = writeUint(w, intValue)
} else {
n, err = writeOpenMetricsFloat(w, floatValue)
}
written += n
if err != nil {
return written, err
}
if metric.TimestampMs != nil {
err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
// TODO(beorn7): Format this directly without converting to a float first.
n, err = writeOpenMetricsFloat(w, float64(*metric.TimestampMs)/1000)
written += n
if err != nil {
return written, err
}
}
if exemplar != nil {
n, err = writeExemplar(w, exemplar)
written += n
if err != nil {
return written, err
}
}
err = w.WriteByte('\n')
written++
if err != nil {
return written, err
}
return written, nil
}
// writeOpenMetricsLabelPairs works like writeOpenMetrics but formats the float
// in OpenMetrics style.
func writeOpenMetricsLabelPairs(
w enhancedWriter,
in []*dto.LabelPair,
additionalLabelName string, additionalLabelValue float64,
) (int, error) {
if len(in) == 0 && additionalLabelName == "" {
return 0, nil
}
var (
written int
separator byte = '{'
)
for _, lp := range in {
err := w.WriteByte(separator)
written++
if err != nil {
return written, err
}
n, err := w.WriteString(lp.GetName())
written += n
if err != nil {
return written, err
}
n, err = w.WriteString(`="`)
written += n
if err != nil {
return written, err
}
n, err = writeEscapedString(w, lp.GetValue(), true)
written += n
if err != nil {
return written, err
}
err = w.WriteByte('"')
written++
if err != nil {
return written, err
}
separator = ','
}
if additionalLabelName != "" {
err := w.WriteByte(separator)
written++
if err != nil {
return written, err
}
n, err := w.WriteString(additionalLabelName)
written += n
if err != nil {
return written, err
}
n, err = w.WriteString(`="`)
written += n
if err != nil {
return written, err
}
n, err = writeOpenMetricsFloat(w, additionalLabelValue)
written += n
if err != nil {
return written, err
}
err = w.WriteByte('"')
written++
if err != nil {
return written, err
}
}
err := w.WriteByte('}')
written++
if err != nil {
return written, err
}
return written, nil
}
// writeExemplar writes the provided exemplar in OpenMetrics format to w. The
// function returns the number of bytes written and any error encountered.
func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) {
written := 0
n, err := w.WriteString(" # ")
written += n
if err != nil {
return written, err
}
n, err = writeOpenMetricsLabelPairs(w, e.Label, "", 0)
written += n
if err != nil {
return written, err
}
err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
n, err = writeOpenMetricsFloat(w, e.GetValue())
written += n
if err != nil {
return written, err
}
if e.Timestamp != nil {
err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
ts, err := ptypes.Timestamp((*e).Timestamp)
if err != nil {
return written, err
}
// TODO(beorn7): Format this directly from components of ts to
// avoid overflow/underflow and precision issues of the float
// conversion.
n, err = writeOpenMetricsFloat(w, float64(ts.UnixNano())/1e9)
written += n
if err != nil {
return written, err
}
}
return written, nil
}
// writeOpenMetricsFloat works like writeFloat but appends ".0" if the resulting
// number would otherwise contain neither a "." nor an "e".
func writeOpenMetricsFloat(w enhancedWriter, f float64) (int, error) {
switch {
case f == 1:
return w.WriteString("1.0")
case f == 0:
return w.WriteString("0.0")
case f == -1:
return w.WriteString("-1.0")
case math.IsNaN(f):
return w.WriteString("NaN")
case math.IsInf(f, +1):
return w.WriteString("+Inf")
case math.IsInf(f, -1):
return w.WriteString("-Inf")
default:
bp := numBufPool.Get().(*[]byte)
*bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64)
if !bytes.ContainsAny(*bp, "e.") {
*bp = append(*bp, '.', '0')
}
written, err := w.Write(*bp)
numBufPool.Put(bp)
return written, err
}
}
// writeUint is like writeInt just for uint64.
func writeUint(w enhancedWriter, u uint64) (int, error) {
bp := numBufPool.Get().(*[]byte)
*bp = strconv.AppendUint((*bp)[:0], u, 10)
written, err := w.Write(*bp)
numBufPool.Put(bp)
return written, err
}

View File

@@ -14,9 +14,10 @@
package expfmt
import (
"bytes"
"bufio"
"fmt"
"io"
"io/ioutil"
"math"
"strconv"
"strings"
@@ -27,7 +28,7 @@ import (
dto "github.com/prometheus/client_model/go"
)
// enhancedWriter has all the enhanced write functions needed here. bytes.Buffer
// enhancedWriter has all the enhanced write functions needed here. bufio.Writer
// implements it.
type enhancedWriter interface {
io.Writer
@@ -37,14 +38,13 @@ type enhancedWriter interface {
}
const (
initialBufSize = 512
initialNumBufSize = 24
)
var (
bufPool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, initialBufSize))
return bufio.NewWriter(ioutil.Discard)
},
}
numBufPool = sync.Pool{
@@ -75,16 +75,14 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
}
// Try the interface upgrade. If it doesn't work, we'll use a
// bytes.Buffer from the sync.Pool and write out its content to out in a
// single go in the end.
// bufio.Writer from the sync.Pool.
w, ok := out.(enhancedWriter)
if !ok {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
b := bufPool.Get().(*bufio.Writer)
b.Reset(out)
w = b
defer func() {
bWritten, bErr := out.Write(b.Bytes())
written = bWritten
bErr := b.Flush()
if err == nil {
err = bErr
}
@@ -425,9 +423,8 @@ var (
func writeEscapedString(w enhancedWriter, v string, includeDoubleQuote bool) (int, error) {
if includeDoubleQuote {
return quotedEscaper.WriteString(w, v)
} else {
return escaper.WriteString(w, v)
}
return escaper.WriteString(w, v)
}
// writeFloat is equivalent to fmt.Fprint with a float64 argument but hardcodes

View File

@@ -325,7 +325,7 @@ func (p *TextParser) startLabelValue() stateFn {
// - Other labels have to be added to currentLabels for signature calculation.
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
if p.currentLabelPair.GetName() == model.QuantileLabel {
if p.currentQuantile, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil {
if p.currentQuantile, p.err = parseFloat(p.currentLabelPair.GetValue()); p.err != nil {
// Create a more helpful error message.
p.parseError(fmt.Sprintf("expected float as value for 'quantile' label, got %q", p.currentLabelPair.GetValue()))
return nil
@@ -337,7 +337,7 @@ func (p *TextParser) startLabelValue() stateFn {
// Similar special treatment of histograms.
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
if p.currentLabelPair.GetName() == model.BucketLabel {
if p.currentBucket, p.err = strconv.ParseFloat(p.currentLabelPair.GetValue(), 64); p.err != nil {
if p.currentBucket, p.err = parseFloat(p.currentLabelPair.GetValue()); p.err != nil {
// Create a more helpful error message.
p.parseError(fmt.Sprintf("expected float as value for 'le' label, got %q", p.currentLabelPair.GetValue()))
return nil
@@ -392,7 +392,7 @@ func (p *TextParser) readingValue() stateFn {
if p.readTokenUntilWhitespace(); p.err != nil {
return nil // Unexpected end of input.
}
value, err := strconv.ParseFloat(p.currentToken.String(), 64)
value, err := parseFloat(p.currentToken.String())
if err != nil {
// Create a more helpful error message.
p.parseError(fmt.Sprintf("expected float as value, got %q", p.currentToken.String()))
@@ -755,3 +755,10 @@ func histogramMetricName(name string) string {
return name
}
}
func parseFloat(s string) (float64, error) {
if strings.ContainsAny(s, "pP_") {
return 0, fmt.Errorf("unsupported character in float")
}
return strconv.ParseFloat(s, 64)
}

View File

@@ -1,12 +1,12 @@
/*
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
HTTP Content-Type Autonegotiation.
The functions in this package implement the behaviour specified in
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

View File

@@ -21,7 +21,6 @@ import (
)
var (
separator = []byte{0}
// MetricNameRE is a regular expression matching valid metric
// names. Note that the IsValidMetricName function performs the same
// check but faster than a match with this regular expression.

View File

@@ -150,7 +150,13 @@ func (t *Time) UnmarshalJSON(b []byte) error {
return err
}
*t = Time(v + va)
// If the value was something like -0.1 the negative is lost in the
// parsing because of the leading zero, this ensures that we capture it.
if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 {
*t = Time(v+va) * -1
} else {
*t = Time(v + va)
}
default:
return fmt.Errorf("invalid time %q", string(b))
@@ -180,6 +186,10 @@ var durationRE = regexp.MustCompile("^([0-9]+)(y|w|d|h|m|s|ms)$")
// ParseDuration parses a string into a time.Duration, assuming that a year
// always has 365d, a week always has 7d, and a day always has 24h.
func ParseDuration(durationStr string) (Duration, error) {
// Allow 0 without a unit.
if durationStr == "0" {
return 0, nil
}
matches := durationRE.FindStringSubmatch(durationStr)
if len(matches) != 3 {
return 0, fmt.Errorf("not a valid duration string: %q", durationStr)