diff --git a/probe/probe_internal_test.go b/probe/probe_internal_test.go index 6578315f0..55cd52e68 100644 --- a/probe/probe_internal_test.go +++ b/probe/probe_internal_test.go @@ -77,10 +77,6 @@ func TestProbe(t *testing.T) { want := report.MakeReport() node := report.MakeNodeWith("a", map[string]string{"b": "c"}) - // DeepEqual doesn't handle the location of timestamps correctly - // See https://github.com/golang/go/issues/10089 - node.Controls.Timestamp = time.Now() - // marshalling->unmarshaling is not idempotent due to `json:"omitempty"` // tags, transforming empty slices into nils. So, we make DeepEqual // happy by setting empty `json:"omitempty"` entries to nil diff --git a/report/controls.go b/report/controls.go index e6a8e0e1b..6e21b9959 100644 --- a/report/controls.go +++ b/report/controls.go @@ -3,6 +3,7 @@ package report import ( "time" + "github.com/ugorji/go/codec" "github.com/weaveworks/scope/common/mtime" ) @@ -83,3 +84,39 @@ func (nc NodeControls) Add(ids ...string) NodeControls { Controls: nc.Controls.Add(ids...), } } + +// WireNodeControls is the intermediate type for json encoding. +type WireNodeControls struct { + Timestamp string `json:"timestamp,omitempty"` + Controls StringSet `json:"controls,omitempty"` +} + +// CodecEncodeSelf implements codec.Selfer +func (nc *NodeControls) CodecEncodeSelf(encoder *codec.Encoder) { + encoder.Encode(WireNodeControls{ + Timestamp: renderTime(nc.Timestamp), + Controls: nc.Controls, + }) +} + +// CodecDecodeSelf implements codec.Selfer +func (nc *NodeControls) CodecDecodeSelf(decoder *codec.Decoder) { + in := WireNodeControls{} + if err := decoder.Decode(&in); err != nil { + return + } + *nc = NodeControls{ + Timestamp: parseTime(in.Timestamp), + Controls: in.Controls, + } +} + +// MarshalJSON shouldn't be used, use CodecEncodeSelf instead +func (NodeControls) MarshalJSON() ([]byte, error) { + panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead") +} + +// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead +func (*NodeControls) UnmarshalJSON(b []byte) error { + panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead") +} diff --git a/report/metric_row.go b/report/metric_row.go index 869c78f71..ad965175e 100644 --- a/report/metric_row.go +++ b/report/metric_row.go @@ -1,8 +1,6 @@ package report import ( - "time" - "github.com/ugorji/go/codec" ) @@ -57,22 +55,23 @@ func (*MetricRow) UnmarshalJSON(b []byte) error { } type wiredMetricRow struct { - ID string `json:"id"` - Label string `json:"label"` - Format string `json:"format,omitempty"` - Group string `json:"group,omitempty"` - Value float64 `json:"value"` - Priority float64 `json:"priority,omitempty"` - Samples []Sample `json:"samples"` - Min float64 `json:"min"` - Max float64 `json:"max"` - First time.Time `json:"first,omitempty"` - Last time.Time `json:"last,omitempty"` + ID string `json:"id"` + Label string `json:"label"` + Format string `json:"format,omitempty"` + Group string `json:"group,omitempty"` + Value float64 `json:"value"` + Priority float64 `json:"priority,omitempty"` + Samples []Sample `json:"samples"` + Min float64 `json:"min"` + Max float64 `json:"max"` + First string `json:"first,omitempty"` + Last string `json:"last,omitempty"` } // CodecEncodeSelf marshals this MetricRow. It takes the basic Metric // rendering, then adds some row-specific fields. func (m *MetricRow) CodecEncodeSelf(encoder *codec.Encoder) { + in := m.Metric.ToIntermediate() encoder.Encode(wiredMetricRow{ ID: m.ID, Label: m.Label, @@ -80,11 +79,11 @@ func (m *MetricRow) CodecEncodeSelf(encoder *codec.Encoder) { Group: m.Group, Value: m.Value, Priority: m.Priority, - Samples: m.Metric.Samples, - Min: m.Metric.Min, - Max: m.Metric.Max, - First: m.Metric.First, - Last: m.Metric.Last, + Samples: in.Samples, + Min: in.Min, + Max: in.Max, + First: in.First, + Last: in.Last, }) } @@ -92,6 +91,14 @@ func (m *MetricRow) CodecEncodeSelf(encoder *codec.Encoder) { func (m *MetricRow) CodecDecodeSelf(decoder *codec.Decoder) { var in wiredMetricRow decoder.Decode(&in) + w := WireMetrics{ + Samples: in.Samples, + Min: in.Min, + Max: in.Max, + First: in.First, + Last: in.Last, + } + metric := w.FromIntermediate() *m = MetricRow{ ID: in.ID, Label: in.Label, @@ -99,13 +106,7 @@ func (m *MetricRow) CodecDecodeSelf(decoder *codec.Decoder) { Group: in.Group, Value: in.Value, Priority: in.Priority, - Metric: &Metric{ - Samples: in.Samples, - Min: in.Min, - Max: in.Max, - First: in.First, - Last: in.Last, - }, + Metric: &metric, } } diff --git a/report/metrics.go b/report/metrics.go index 7a766831d..ef684437f 100644 --- a/report/metrics.go +++ b/report/metrics.go @@ -3,6 +3,8 @@ package report import ( "math" "time" + + "github.com/ugorji/go/codec" ) // Metrics is a string->metric map. @@ -36,11 +38,9 @@ func (m Metrics) Copy() Metrics { // Metric is a list of timeseries data with some metadata. Clients must use the // Add method to add values. Metrics are immutable. type Metric struct { - Samples []Sample `json:"samples"` - Min float64 `json:"min"` - Max float64 `json:"max"` - First time.Time `json:"first"` - Last time.Time `json:"last"` + Samples []Sample + Min, Max float64 + First, Last time.Time } // Sample is a single datapoint of a metric. @@ -206,3 +206,73 @@ func (m Metric) LastSample() (Sample, bool) { } return m.Samples[len(m.Samples)-1], true } + +// WireMetrics is the on-the-wire representation of Metrics. +type WireMetrics struct { + Samples []Sample `json:"samples,omitempty"` // On the wire, samples are sorted oldest to newest, + Min float64 `json:"min"` // the opposite order to how we store them internally. + Max float64 `json:"max"` + First string `json:"first,omitempty"` + Last string `json:"last,omitempty"` +} + +func renderTime(t time.Time) string { + if t.IsZero() { + return "" + } + return t.Format(time.RFC3339Nano) +} + +func parseTime(s string) time.Time { + t, _ := time.Parse(time.RFC3339Nano, s) + return t +} + +// ToIntermediate converts the metric to a representation suitable +// for serialization. +func (m Metric) ToIntermediate() WireMetrics { + return WireMetrics{ + Samples: m.Samples, + Max: m.Max, + Min: m.Min, + First: renderTime(m.First), + Last: renderTime(m.Last), + } +} + +// FromIntermediate obtains the metric from a representation suitable +// for serialization. +func (m WireMetrics) FromIntermediate() Metric { + return Metric{ + Samples: m.Samples, + Max: m.Max, + Min: m.Min, + First: parseTime(m.First), + Last: parseTime(m.Last), + } +} + +// CodecEncodeSelf implements codec.Selfer +func (m *Metric) CodecEncodeSelf(encoder *codec.Encoder) { + in := m.ToIntermediate() + encoder.Encode(in) +} + +// CodecDecodeSelf implements codec.Selfer +func (m *Metric) CodecDecodeSelf(decoder *codec.Decoder) { + in := WireMetrics{} + if err := decoder.Decode(&in); err != nil { + return + } + *m = in.FromIntermediate() +} + +// MarshalJSON shouldn't be used, use CodecEncodeSelf instead +func (Metric) MarshalJSON() ([]byte, error) { + panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead") +} + +// UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead +func (*Metric) UnmarshalJSON(b []byte) error { + panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead") +}