Modernize invertergui: MQTT write support, HA integration, UI updates
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled

This commit is contained in:
2026-02-19 12:03:52 +11:00
parent 959d1e3c1f
commit a31a0b4829
460 changed files with 19655 additions and 40205 deletions

View File

@@ -14,6 +14,7 @@
package model
import (
"errors"
"fmt"
"time"
)
@@ -64,7 +65,7 @@ func (a *Alert) Resolved() bool {
return a.ResolvedAt(time.Now())
}
// ResolvedAt returns true off the activity interval ended before
// ResolvedAt returns true iff the activity interval ended before
// the given timestamp.
func (a *Alert) ResolvedAt(ts time.Time) bool {
if a.EndsAt.IsZero() {
@@ -75,7 +76,12 @@ func (a *Alert) ResolvedAt(ts time.Time) bool {
// Status returns the status of the alert.
func (a *Alert) Status() AlertStatus {
if a.Resolved() {
return a.StatusAt(time.Now())
}
// StatusAt returns the status of the alert at the given timestamp.
func (a *Alert) StatusAt(ts time.Time) AlertStatus {
if a.ResolvedAt(ts) {
return AlertResolved
}
return AlertFiring
@@ -84,16 +90,16 @@ func (a *Alert) Status() AlertStatus {
// Validate checks whether the alert data is inconsistent.
func (a *Alert) Validate() error {
if a.StartsAt.IsZero() {
return fmt.Errorf("start time missing")
return errors.New("start time missing")
}
if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) {
return fmt.Errorf("start time must be before end time")
return errors.New("start time must be before end time")
}
if err := a.Labels.Validate(); err != nil {
return fmt.Errorf("invalid label set: %w", err)
}
if len(a.Labels) == 0 {
return fmt.Errorf("at least one label pair required")
return errors.New("at least one label pair required")
}
if err := a.Annotations.Validate(); err != nil {
return fmt.Errorf("invalid annotations: %w", err)
@@ -127,6 +133,17 @@ func (as Alerts) HasFiring() bool {
return false
}
// HasFiringAt returns true iff one of the alerts is not resolved
// at the time ts.
func (as Alerts) HasFiringAt(ts time.Time) bool {
for _, a := range as {
if !a.ResolvedAt(ts) {
return true
}
}
return false
}
// Status returns StatusFiring iff at least one of the alerts is firing.
func (as Alerts) Status() AlertStatus {
if as.HasFiring() {
@@ -134,3 +151,12 @@ func (as Alerts) Status() AlertStatus {
}
return AlertResolved
}
// StatusAt returns StatusFiring iff at least one of the alerts is firing
// at the time ts.
func (as Alerts) StatusAt(ts time.Time) AlertStatus {
if as.HasFiringAt(ts) {
return AlertFiring
}
return AlertResolved
}

View File

@@ -22,7 +22,7 @@ import (
)
const (
// AlertNameLabel is the name of the label containing the an alert's name.
// AlertNameLabel is the name of the label containing the alert's name.
AlertNameLabel = "alertname"
// ExportedLabelPrefix is the prefix to prepend to the label names present in
@@ -32,6 +32,12 @@ const (
// MetricNameLabel is the label name indicating the metric name of a
// timeseries.
MetricNameLabel = "__name__"
// MetricTypeLabel is the label name indicating the metric type of
// timeseries as per the PROM-39 proposal.
MetricTypeLabel = "__type__"
// MetricUnitLabel is the label name indicating the metric unit of
// timeseries as per the PROM-39 proposal.
MetricUnitLabel = "__unit__"
// SchemeLabel is the name of the label that holds the scheme on which to
// scrape a target.
@@ -97,27 +103,24 @@ var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
// therewith.
type LabelName string
// IsValid returns true iff name matches the pattern of LabelNameRE for legacy
// names, and iff it's valid UTF-8 if NameValidationScheme is set to
// UTF8Validation. For the legacy matching, it does not use LabelNameRE for the
// check but a much faster hardcoded implementation.
// IsValid returns true iff the name matches the pattern of LabelNameRE when
// NameValidationScheme is set to LegacyValidation, or valid UTF-8 if
// NameValidationScheme is set to UTF8Validation.
//
// Deprecated: This method should not be used and may be removed in the future.
// Use [ValidationScheme.IsValidLabelName] instead.
func (ln LabelName) IsValid() bool {
if len(ln) == 0 {
return false
}
switch NameValidationScheme {
case LegacyValidation:
for i, b := range ln {
if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) {
return false
}
}
case UTF8Validation:
return utf8.ValidString(string(ln))
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme))
}
return true
return NameValidationScheme.IsValidLabelName(string(ln))
}
// IsValidLegacy returns true iff name matches the pattern of LabelNameRE for
// legacy names. It does not use LabelNameRE for the check but a much faster
// hardcoded implementation.
//
// Deprecated: This method should not be used and may be removed in the future.
// Use [LegacyValidation.IsValidLabelName] instead.
func (ln LabelName) IsValidLegacy() bool {
return LegacyValidation.IsValidLabelName(string(ln))
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.

View File

@@ -14,12 +14,9 @@
package model
import (
"bytes"
"encoding/json"
"fmt"
"slices"
"sort"
"strconv"
)
// A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet
@@ -117,10 +114,10 @@ func (ls LabelSet) Clone() LabelSet {
}
// Merge is a helper function to non-destructively merge two label sets.
func (l LabelSet) Merge(other LabelSet) LabelSet {
result := make(LabelSet, len(l))
func (ls LabelSet) Merge(other LabelSet) LabelSet {
result := make(LabelSet, len(ls))
for k, v := range l {
for k, v := range ls {
result[k] = v
}
@@ -131,29 +128,6 @@ func (l LabelSet) Merge(other LabelSet) LabelSet {
return result
}
// String will look like `{foo="bar", more="less"}`. Names are sorted alphabetically.
func (l LabelSet) String() string {
var lna [32]LabelName // On stack to avoid memory allocation for sorting names.
labelNames := lna[:0]
for name := range l {
labelNames = append(labelNames, name)
}
slices.Sort(labelNames)
var bytea [1024]byte // On stack to avoid memory allocation while building the output.
b := bytes.NewBuffer(bytea[:0])
b.WriteByte('{')
for i, name := range labelNames {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(string(name))
b.WriteByte('=')
b.Write(strconv.AppendQuote(b.AvailableBuffer(), string(l[name])))
}
b.WriteByte('}')
return b.String()
}
// Fingerprint returns the LabelSet's fingerprint.
func (ls LabelSet) Fingerprint() Fingerprint {
return labelSetToFingerprint(ls)
@@ -166,7 +140,7 @@ func (ls LabelSet) FastFingerprint() Fingerprint {
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (l *LabelSet) UnmarshalJSON(b []byte) error {
func (ls *LabelSet) UnmarshalJSON(b []byte) error {
var m map[LabelName]LabelValue
if err := json.Unmarshal(b, &m); err != nil {
return err
@@ -179,6 +153,6 @@ func (l *LabelSet) UnmarshalJSON(b []byte) error {
return fmt.Errorf("%q is not a valid label name", ln)
}
}
*l = LabelSet(m)
*ls = LabelSet(m)
return nil
}

View File

@@ -14,30 +14,49 @@
package model
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"unicode/utf8"
dto "github.com/prometheus/client_model/go"
"go.yaml.in/yaml/v2"
"google.golang.org/protobuf/proto"
)
var (
// NameValidationScheme determines the method of name validation to be used by
// all calls to IsValidMetricName() and LabelName IsValid(). Setting UTF-8 mode
// in isolation from other components that don't support UTF-8 may result in
// bugs or other undefined behavior. This value is intended to be set by
// UTF-8-aware binaries as part of their startup. To avoid need for locking,
// this value should be set once, ideally in an init(), before multiple
// goroutines are started.
NameValidationScheme = LegacyValidation
// NameValidationScheme determines the global default method of the name
// validation to be used by all calls to IsValidMetricName() and LabelName
// IsValid().
//
// Deprecated: This variable should not be used and might be removed in the
// far future. If you wish to stick to the legacy name validation use
// `IsValidLegacyMetricName()` and `LabelName.IsValidLegacy()` methods
// instead. This variable is here as an escape hatch for emergency cases,
// given the recent change from `LegacyValidation` to `UTF8Validation`, e.g.,
// to delay UTF-8 migrations in time or aid in debugging unforeseen results of
// the change. In such a case, a temporary assignment to `LegacyValidation`
// value in the `init()` function in your main.go or so, could be considered.
//
// Historically we opted for a global variable for feature gating different
// validation schemes in operations that were not otherwise easily adjustable
// (e.g. Labels yaml unmarshaling). That could have been a mistake, a separate
// Labels structure or package might have been a better choice. Given the
// change was made and many upgraded the common already, we live this as-is
// with this warning and learning for the future.
NameValidationScheme = UTF8Validation
// NameEscapingScheme defines the default way that names will be
// escaped when presented to systems that do not support UTF-8 names. If the
// Content-Type "escaping" term is specified, that will override this value.
NameEscapingScheme = ValueEncodingEscaping
// NameEscapingScheme defines the default way that names will be escaped when
// presented to systems that do not support UTF-8 names. If the Content-Type
// "escaping" term is specified, that will override this value.
// NameEscapingScheme should not be set to the NoEscaping value. That string
// is used in content negotiation to indicate that a system supports UTF-8 and
// has that feature enabled.
NameEscapingScheme = UnderscoreEscaping
)
// ValidationScheme is a Go enum for determining how metric and label names will
@@ -45,16 +64,151 @@ var (
type ValidationScheme int
const (
// LegacyValidation is a setting that requirets that metric and label names
// UnsetValidation represents an undefined ValidationScheme.
// Should not be used in practice.
UnsetValidation ValidationScheme = iota
// LegacyValidation is a setting that requires that all metric and label names
// conform to the original Prometheus character requirements described by
// MetricNameRE and LabelNameRE.
LegacyValidation ValidationScheme = iota
LegacyValidation
// UTF8Validation only requires that metric and label names be valid UTF-8
// strings.
UTF8Validation
)
var _ interface {
yaml.Marshaler
yaml.Unmarshaler
json.Marshaler
json.Unmarshaler
fmt.Stringer
} = new(ValidationScheme)
// String returns the string representation of s.
func (s ValidationScheme) String() string {
switch s {
case UnsetValidation:
return "unset"
case LegacyValidation:
return "legacy"
case UTF8Validation:
return "utf8"
default:
panic(fmt.Errorf("unhandled ValidationScheme: %d", s))
}
}
// MarshalYAML implements the yaml.Marshaler interface.
func (s ValidationScheme) MarshalYAML() (any, error) {
switch s {
case UnsetValidation:
return "", nil
case LegacyValidation, UTF8Validation:
return s.String(), nil
default:
panic(fmt.Errorf("unhandled ValidationScheme: %d", s))
}
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *ValidationScheme) UnmarshalYAML(unmarshal func(any) error) error {
var scheme string
if err := unmarshal(&scheme); err != nil {
return err
}
return s.Set(scheme)
}
// MarshalJSON implements the json.Marshaler interface.
func (s ValidationScheme) MarshalJSON() ([]byte, error) {
switch s {
case UnsetValidation:
return json.Marshal("")
case UTF8Validation, LegacyValidation:
return json.Marshal(s.String())
default:
return nil, fmt.Errorf("unhandled ValidationScheme: %d", s)
}
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (s *ValidationScheme) UnmarshalJSON(bytes []byte) error {
var repr string
if err := json.Unmarshal(bytes, &repr); err != nil {
return err
}
return s.Set(repr)
}
// Set implements the pflag.Value interface.
func (s *ValidationScheme) Set(text string) error {
switch text {
case "":
// Don't change the value.
case LegacyValidation.String():
*s = LegacyValidation
case UTF8Validation.String():
*s = UTF8Validation
default:
return fmt.Errorf("unrecognized ValidationScheme: %q", text)
}
return nil
}
// IsValidMetricName returns whether metricName is valid according to s.
func (s ValidationScheme) IsValidMetricName(metricName string) bool {
switch s {
case LegacyValidation:
if len(metricName) == 0 {
return false
}
for i, b := range metricName {
if !isValidLegacyRune(b, i) {
return false
}
}
return true
case UTF8Validation:
if len(metricName) == 0 {
return false
}
return utf8.ValidString(metricName)
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %s", s.String()))
}
}
// IsValidLabelName returns whether labelName is valid according to s.
func (s ValidationScheme) IsValidLabelName(labelName string) bool {
switch s {
case LegacyValidation:
if len(labelName) == 0 {
return false
}
for i, b := range labelName {
// TODO: Apply De Morgan's law. Make sure there are tests for this.
if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { //nolint:staticcheck
return false
}
}
return true
case UTF8Validation:
if len(labelName) == 0 {
return false
}
return utf8.ValidString(labelName)
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %s", s))
}
}
// Type implements the pflag.Value interface.
func (ValidationScheme) Type() string {
return "validationScheme"
}
type EscapingScheme int
const (
@@ -84,7 +238,7 @@ const (
// Accept header, the default NameEscapingScheme will be used.
EscapingKey = "escaping"
// Possible values for Escaping Key:
// Possible values for Escaping Key.
AllowUTF8 = "allow-utf-8" // No escaping required.
EscapeUnderscores = "underscores"
EscapeDots = "dots"
@@ -158,34 +312,22 @@ func (m Metric) FastFingerprint() Fingerprint {
// IsValidMetricName returns true iff name matches the pattern of MetricNameRE
// for legacy names, and iff it's valid UTF-8 if the UTF8Validation scheme is
// selected.
//
// Deprecated: This function should not be used and might be removed in the future.
// Use [ValidationScheme.IsValidMetricName] instead.
func IsValidMetricName(n LabelValue) bool {
switch NameValidationScheme {
case LegacyValidation:
return IsValidLegacyMetricName(n)
case UTF8Validation:
if len(n) == 0 {
return false
}
return utf8.ValidString(string(n))
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme))
}
return NameValidationScheme.IsValidMetricName(string(n))
}
// IsValidLegacyMetricName is similar to IsValidMetricName but always uses the
// legacy validation scheme regardless of the value of NameValidationScheme.
// This function, however, does not use MetricNameRE for the check but a much
// faster hardcoded implementation.
func IsValidLegacyMetricName(n LabelValue) bool {
if len(n) == 0 {
return false
}
for i, b := range n {
if !isValidLegacyRune(b, i) {
return false
}
}
return true
//
// Deprecated: This function should not be used and might be removed in the future.
// Use [LegacyValidation.IsValidMetricName] instead.
func IsValidLegacyMetricName(n string) bool {
return LegacyValidation.IsValidMetricName(n)
}
// EscapeMetricFamily escapes the given metric names and labels with the given
@@ -208,7 +350,7 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
}
// If the name is nil, copy as-is, don't try to escape.
if v.Name == nil || IsValidLegacyMetricName(LabelValue(v.GetName())) {
if v.Name == nil || IsValidLegacyMetricName(v.GetName()) {
out.Name = v.Name
} else {
out.Name = proto.String(EscapeName(v.GetName(), scheme))
@@ -230,7 +372,7 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
for _, l := range m.Label {
if l.GetName() == MetricNameLabel {
if l.Value == nil || IsValidLegacyMetricName(LabelValue(l.GetValue())) {
if l.Value == nil || IsValidLegacyMetricName(l.GetValue()) {
escaped.Label = append(escaped.Label, l)
continue
}
@@ -240,7 +382,7 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
})
continue
}
if l.Name == nil || IsValidLegacyMetricName(LabelValue(l.GetName())) {
if l.Name == nil || IsValidLegacyMetricName(l.GetName()) {
escaped.Label = append(escaped.Label, l)
continue
}
@@ -256,20 +398,16 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
func metricNeedsEscaping(m *dto.Metric) bool {
for _, l := range m.Label {
if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(LabelValue(l.GetValue())) {
if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(l.GetValue()) {
return true
}
if !IsValidLegacyMetricName(LabelValue(l.GetName())) {
if !IsValidLegacyMetricName(l.GetName()) {
return true
}
}
return false
}
const (
lowerhex = "0123456789abcdef"
)
// EscapeName escapes the incoming name according to the provided escaping
// scheme. Depending on the rules of escaping, this may cause no change in the
// string that is returned. (Especially NoEscaping, which by definition is a
@@ -283,7 +421,7 @@ func EscapeName(name string, scheme EscapingScheme) string {
case NoEscaping:
return name
case UnderscoreEscaping:
if IsValidLegacyMetricName(LabelValue(name)) {
if IsValidLegacyMetricName(name) {
return name
}
for i, b := range name {
@@ -297,38 +435,34 @@ func EscapeName(name string, scheme EscapingScheme) string {
case DotsEscaping:
// Do not early return for legacy valid names, we still escape underscores.
for i, b := range name {
if b == '_' {
switch {
case b == '_':
escaped.WriteString("__")
} else if b == '.' {
case b == '.':
escaped.WriteString("_dot_")
} else if isValidLegacyRune(b, i) {
case isValidLegacyRune(b, i):
escaped.WriteRune(b)
} else {
escaped.WriteRune('_')
default:
escaped.WriteString("__")
}
}
return escaped.String()
case ValueEncodingEscaping:
if IsValidLegacyMetricName(LabelValue(name)) {
if IsValidLegacyMetricName(name) {
return name
}
escaped.WriteString("U__")
for i, b := range name {
if isValidLegacyRune(b, i) {
switch {
case b == '_':
escaped.WriteString("__")
case isValidLegacyRune(b, i):
escaped.WriteRune(b)
} else if !utf8.ValidRune(b) {
case !utf8.ValidRune(b):
escaped.WriteString("_FFFD_")
} else if b < 0x100 {
default:
escaped.WriteRune('_')
for s := 4; s >= 0; s -= 4 {
escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
}
escaped.WriteRune('_')
} else if b < 0x10000 {
escaped.WriteRune('_')
for s := 12; s >= 0; s -= 4 {
escaped.WriteByte(lowerhex[b>>uint(s)&0xF])
}
escaped.WriteString(strconv.FormatInt(int64(b), 16))
escaped.WriteRune('_')
}
}
@@ -338,7 +472,7 @@ func EscapeName(name string, scheme EscapingScheme) string {
}
}
// lower function taken from strconv.atoi
// lower function taken from strconv.atoi.
func lower(c byte) byte {
return c | ('x' - 'X')
}
@@ -386,8 +520,9 @@ func UnescapeName(name string, scheme EscapingScheme) string {
// We think we are in a UTF-8 code, process it.
var utf8Val uint
for j := 0; i < len(escapedName); j++ {
// This is too many characters for a utf8 value.
if j > 4 {
// This is too many characters for a utf8 value based on the MaxRune
// value of '\U0010FFFF'.
if j >= 6 {
return name
}
// Found a closing underscore, convert to a rune, check validity, and append.
@@ -401,11 +536,12 @@ func UnescapeName(name string, scheme EscapingScheme) string {
}
r := lower(escapedName[i])
utf8Val *= 16
if r >= '0' && r <= '9' {
switch {
case r >= '0' && r <= '9':
utf8Val += uint(r) - '0'
} else if r >= 'a' && r <= 'f' {
case r >= 'a' && r <= 'f':
utf8Val += uint(r) - 'a' + 10
} else {
default:
return name
}
i++
@@ -440,7 +576,7 @@ func (e EscapingScheme) String() string {
func ToEscapingScheme(s string) (EscapingScheme, error) {
if s == "" {
return NoEscaping, fmt.Errorf("got empty string instead of escaping scheme")
return NoEscaping, errors.New("got empty string instead of escaping scheme")
}
switch s {
case AllowUTF8:
@@ -452,6 +588,6 @@ func ToEscapingScheme(s string) (EscapingScheme, error) {
case EscapeValues:
return ValueEncodingEscaping, nil
default:
return NoEscaping, fmt.Errorf("unknown format scheme " + s)
return NoEscaping, fmt.Errorf("unknown format scheme %s", s)
}
}

View File

@@ -15,6 +15,7 @@ package model
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"time"
@@ -34,7 +35,7 @@ func (m *Matcher) UnmarshalJSON(b []byte) error {
}
if len(m.Name) == 0 {
return fmt.Errorf("label name in matcher must not be empty")
return errors.New("label name in matcher must not be empty")
}
if m.IsRegex {
if _, err := regexp.Compile(m.Value); err != nil {
@@ -77,7 +78,7 @@ type Silence struct {
// Validate returns true iff all fields of the silence have valid values.
func (s *Silence) Validate() error {
if len(s.Matchers) == 0 {
return fmt.Errorf("at least one matcher required")
return errors.New("at least one matcher required")
}
for _, m := range s.Matchers {
if err := m.Validate(); err != nil {
@@ -85,22 +86,22 @@ func (s *Silence) Validate() error {
}
}
if s.StartsAt.IsZero() {
return fmt.Errorf("start time missing")
return errors.New("start time missing")
}
if s.EndsAt.IsZero() {
return fmt.Errorf("end time missing")
return errors.New("end time missing")
}
if s.EndsAt.Before(s.StartsAt) {
return fmt.Errorf("start time must be before end time")
return errors.New("start time must be before end time")
}
if s.CreatedBy == "" {
return fmt.Errorf("creator information missing")
return errors.New("creator information missing")
}
if s.Comment == "" {
return fmt.Errorf("comment missing")
return errors.New("comment missing")
}
if s.CreatedAt.IsZero() {
return fmt.Errorf("creation timestamp missing")
return errors.New("creation timestamp missing")
}
return nil
}

View File

@@ -126,14 +126,14 @@ func (t *Time) UnmarshalJSON(b []byte) error {
p := strings.Split(string(b), ".")
switch len(p) {
case 1:
v, err := strconv.ParseInt(string(p[0]), 10, 64)
v, err := strconv.ParseInt(p[0], 10, 64)
if err != nil {
return err
}
*t = Time(v * second)
case 2:
v, err := strconv.ParseInt(string(p[0]), 10, 64)
v, err := strconv.ParseInt(p[0], 10, 64)
if err != nil {
return err
}
@@ -143,7 +143,7 @@ func (t *Time) UnmarshalJSON(b []byte) error {
if prec < 0 {
p[1] = p[1][:dotPrecision]
} else if prec > 0 {
p[1] = p[1] + strings.Repeat("0", prec)
p[1] += strings.Repeat("0", prec)
}
va, err := strconv.ParseInt(p[1], 10, 32)
@@ -170,15 +170,15 @@ func (t *Time) UnmarshalJSON(b []byte) error {
// This type should not propagate beyond the scope of input/output processing.
type Duration time.Duration
// Set implements pflag/flag.Value
// Set implements pflag/flag.Value.
func (d *Duration) Set(s string) error {
var err error
*d, err = ParseDuration(s)
return err
}
// Type implements pflag.Value
func (d *Duration) Type() string {
// Type implements pflag.Value.
func (*Duration) Type() string {
return "duration"
}
@@ -201,6 +201,7 @@ var unitMap = map[string]struct {
// 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.
// Negative durations are not supported.
func ParseDuration(s string) (Duration, error) {
switch s {
case "0":
@@ -253,18 +254,36 @@ func ParseDuration(s string) (Duration, error) {
return 0, errors.New("duration out of range")
}
}
return Duration(dur), nil
}
// ParseDurationAllowNegative is like ParseDuration but also accepts negative durations.
func ParseDurationAllowNegative(s string) (Duration, error) {
if s == "" || s[0] != '-' {
return ParseDuration(s)
}
d, err := ParseDuration(s[1:])
return -d, err
}
func (d Duration) String() string {
var (
ms = int64(time.Duration(d) / time.Millisecond)
r = ""
ms = int64(time.Duration(d) / time.Millisecond)
r = ""
sign = ""
)
if ms == 0 {
return "0s"
}
if ms < 0 {
sign, ms = "-", -ms
}
f := func(unit string, mult int64, exact bool) {
if exact && ms%mult != 0 {
return
@@ -286,7 +305,7 @@ func (d Duration) String() string {
f("s", 1000, false)
f("ms", 1, false)
return r
return sign + r
}
// MarshalJSON implements the json.Marshaler interface.

View File

@@ -191,7 +191,8 @@ func (ss SampleStream) String() string {
}
func (ss SampleStream) MarshalJSON() ([]byte, error) {
if len(ss.Histograms) > 0 && len(ss.Values) > 0 {
switch {
case len(ss.Histograms) > 0 && len(ss.Values) > 0:
v := struct {
Metric Metric `json:"metric"`
Values []SamplePair `json:"values"`
@@ -202,7 +203,7 @@ func (ss SampleStream) MarshalJSON() ([]byte, error) {
Histograms: ss.Histograms,
}
return json.Marshal(&v)
} else if len(ss.Histograms) > 0 {
case len(ss.Histograms) > 0:
v := struct {
Metric Metric `json:"metric"`
Histograms []SampleHistogramPair `json:"histograms"`
@@ -211,7 +212,7 @@ func (ss SampleStream) MarshalJSON() ([]byte, error) {
Histograms: ss.Histograms,
}
return json.Marshal(&v)
} else {
default:
v := struct {
Metric Metric `json:"metric"`
Values []SamplePair `json:"values"`
@@ -258,7 +259,7 @@ func (s Scalar) String() string {
// MarshalJSON implements json.Marshaler.
func (s Scalar) MarshalJSON() ([]byte, error) {
v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64)
return json.Marshal([...]interface{}{s.Timestamp, string(v)})
return json.Marshal([...]interface{}{s.Timestamp, v})
}
// UnmarshalJSON implements json.Unmarshaler.
@@ -349,9 +350,9 @@ func (m Matrix) Len() int { return len(m) }
func (m Matrix) Less(i, j int) bool { return m[i].Metric.Before(m[j].Metric) }
func (m Matrix) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (mat Matrix) String() string {
matCp := make(Matrix, len(mat))
copy(matCp, mat)
func (m Matrix) String() string {
matCp := make(Matrix, len(m))
copy(matCp, m)
sort.Sort(matCp)
strs := make([]string, len(matCp))

View File

@@ -15,6 +15,7 @@ package model
import (
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
@@ -39,7 +40,7 @@ func (v SampleValue) MarshalJSON() ([]byte, error) {
// UnmarshalJSON implements json.Unmarshaler.
func (v *SampleValue) UnmarshalJSON(b []byte) error {
if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
return fmt.Errorf("sample value must be a quoted string")
return errors.New("sample value must be a quoted string")
}
f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
if err != nil {

View File

@@ -15,6 +15,7 @@ package model
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
@@ -32,7 +33,7 @@ func (v FloatString) MarshalJSON() ([]byte, error) {
func (v *FloatString) UnmarshalJSON(b []byte) error {
if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
return fmt.Errorf("float value must be a quoted string")
return errors.New("float value must be a quoted string")
}
f, err := strconv.ParseFloat(string(b[1:len(b)-1]), 64)
if err != nil {
@@ -85,22 +86,22 @@ func (s *HistogramBucket) Equal(o *HistogramBucket) bool {
return s == o || (s.Boundaries == o.Boundaries && s.Lower == o.Lower && s.Upper == o.Upper && s.Count == o.Count)
}
func (b HistogramBucket) String() string {
func (s HistogramBucket) String() string {
var sb strings.Builder
lowerInclusive := b.Boundaries == 1 || b.Boundaries == 3
upperInclusive := b.Boundaries == 0 || b.Boundaries == 3
lowerInclusive := s.Boundaries == 1 || s.Boundaries == 3
upperInclusive := s.Boundaries == 0 || s.Boundaries == 3
if lowerInclusive {
sb.WriteRune('[')
} else {
sb.WriteRune('(')
}
fmt.Fprintf(&sb, "%g,%g", b.Lower, b.Upper)
fmt.Fprintf(&sb, "%g,%g", s.Lower, s.Upper)
if upperInclusive {
sb.WriteRune(']')
} else {
sb.WriteRune(')')
}
fmt.Fprintf(&sb, ":%v", b.Count)
fmt.Fprintf(&sb, ":%v", s.Count)
return sb.String()
}
@@ -141,7 +142,7 @@ type SampleHistogramPair struct {
func (s SampleHistogramPair) MarshalJSON() ([]byte, error) {
if s.Histogram == nil {
return nil, fmt.Errorf("histogram is nil")
return nil, errors.New("histogram is nil")
}
t, err := json.Marshal(s.Timestamp)
if err != nil {
@@ -164,7 +165,7 @@ func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error {
return fmt.Errorf("wrong number of fields: %d != %d", gotLen, wantLen)
}
if s.Histogram == nil {
return fmt.Errorf("histogram is null")
return errors.New("histogram is null")
}
return nil
}

View File

@@ -66,8 +66,8 @@ func (et *ValueType) UnmarshalJSON(b []byte) error {
return nil
}
func (e ValueType) String() string {
switch e {
func (et ValueType) String() string {
switch et {
case ValNone:
return "<ValNone>"
case ValScalar: