func AppendRepeatingValuesTests(p metric.Persistence, t test.Tester) { m := clientmodel.Metric{ clientmodel.MetricNameLabel: "errors_total", "controller": "foo", "operation": "bar", } increments := 10 repetitions := 500 for i := 0; i < increments; i++ { for j := 0; j < repetitions; j++ { time := clientmodel.Timestamp(0).Add(time.Duration(i) * time.Hour).Add(time.Duration(j) * time.Second) testAppendSamples(p, &clientmodel.Sample{ Value: clientmodel.SampleValue(i), Timestamp: time, Metric: m, }, t) } } v, ok := p.(metric.View) if !ok { // It's purely a benchmark for a Persistence that is not viewable. return } matchers := labelMatchersFromLabelSet(clientmodel.LabelSet{ clientmodel.MetricNameLabel: "errors_total", "controller": "foo", "operation": "bar", }) for i := 0; i < increments; i++ { for j := 0; j < repetitions; j++ { fingerprints, err := p.GetFingerprintsForLabelMatchers(matchers) if err != nil { t.Fatal(err) } if len(fingerprints) != 1 { t.Fatalf("expected %d fingerprints, got %d", 1, len(fingerprints)) } time := clientmodel.Timestamp(0).Add(time.Duration(i) * time.Hour).Add(time.Duration(j) * time.Second) samples := v.GetValueAtTime(fingerprints[0], time) if len(samples) == 0 { t.Fatal("expected at least one sample.") } expected := clientmodel.SampleValue(i) for _, sample := range samples { if sample.Value != expected { t.Fatalf("expected %v value, got %v", expected, sample.Value) } } } } }
func (c *deltaEncodedChunk) valueAtIndex(idx int) *metric.SamplePair { offset := deltaHeaderBytes + idx*c.sampleSize() var ts clientmodel.Timestamp switch c.timeBytes() { case d1: ts = c.baseTime() + clientmodel.Timestamp(uint8(c.buf[offset])) case d2: ts = c.baseTime() + clientmodel.Timestamp(binary.LittleEndian.Uint16(c.buf[offset:])) case d4: ts = c.baseTime() + clientmodel.Timestamp(binary.LittleEndian.Uint32(c.buf[offset:])) case d8: // Take absolute value for d8. ts = clientmodel.Timestamp(binary.LittleEndian.Uint64(c.buf[offset:])) default: panic("Invalid number of bytes for time delta") } offset += int(c.timeBytes()) var v clientmodel.SampleValue if c.isInt() { switch c.valueBytes() { case d0: v = c.baseValue() case d1: v = c.baseValue() + clientmodel.SampleValue(int8(c.buf[offset])) case d2: v = c.baseValue() + clientmodel.SampleValue(int16(binary.LittleEndian.Uint16(c.buf[offset:]))) case d4: v = c.baseValue() + clientmodel.SampleValue(int32(binary.LittleEndian.Uint32(c.buf[offset:]))) // No d8 for ints. default: panic("Invalid number of bytes for integer delta") } } else { switch c.valueBytes() { case d4: v = c.baseValue() + clientmodel.SampleValue(math.Float32frombits(binary.LittleEndian.Uint32(c.buf[offset:]))) case d8: // Take absolute value for d8. v = clientmodel.SampleValue(math.Float64frombits(binary.LittleEndian.Uint64(c.buf[offset:]))) default: panic("Invalid number of bytes for floating point delta") } } return &metric.SamplePair{ Timestamp: ts, Value: v, } }
// loadChunkDescs loads chunkDescs for a series up until a given time. It is // the caller's responsibility to not persist or drop anything for the same // fingerprint concurrently. func (p *persistence) loadChunkDescs(fp clientmodel.Fingerprint, beforeTime clientmodel.Timestamp) ([]*chunkDesc, error) { f, err := p.openChunkFileForReading(fp) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } defer f.Close() fi, err := f.Stat() if err != nil { return nil, err } totalChunkLen := chunkHeaderLen + p.chunkLen if fi.Size()%int64(totalChunkLen) != 0 { p.setDirty(true) return nil, fmt.Errorf( "size of series file for fingerprint %v is %d, which is not a multiple of the chunk length %d", fp, fi.Size(), totalChunkLen, ) } numChunks := int(fi.Size()) / totalChunkLen cds := make([]*chunkDesc, 0, numChunks) for i := 0; i < numChunks; i++ { _, err := f.Seek(p.offsetForChunkIndex(i)+chunkHeaderFirstTimeOffset, os.SEEK_SET) if err != nil { return nil, err } chunkTimesBuf := make([]byte, 16) _, err = io.ReadAtLeast(f, chunkTimesBuf, 16) if err != nil { return nil, err } cd := &chunkDesc{ chunkFirstTime: clientmodel.Timestamp(binary.LittleEndian.Uint64(chunkTimesBuf)), chunkLastTime: clientmodel.Timestamp(binary.LittleEndian.Uint64(chunkTimesBuf[8:])), } if !cd.chunkLastTime.Before(beforeTime) { // From here on, we have chunkDescs in memory already. break } cds = append(cds, cd) } chunkDescOps.WithLabelValues(load).Add(float64(len(cds))) numMemChunkDescs.Add(float64(len(cds))) return cds, nil }
// UnmarshalBinary implements encoding.BinaryUnmarshaler. func (tr *TimeRange) UnmarshalBinary(buf []byte) error { r := bytes.NewReader(buf) first, err := binary.ReadVarint(r) if err != nil { return err } last, err := binary.ReadVarint(r) if err != nil { return err } tr.First = clientmodel.Timestamp(first) tr.Last = clientmodel.Timestamp(last) return nil }
func (c doubleDeltaEncodedChunk) baseTime() clientmodel.Timestamp { return clientmodel.Timestamp( binary.LittleEndian.Uint64( c[doubleDeltaHeaderBaseTimeOffset:], ), ) }
// TestLoop is just a smoke test for the loop method, if we can switch it on and // off without disaster. func TestLoop(t *testing.T) { samples := make(clientmodel.Samples, 1000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(2 * i), Value: clientmodel.SampleValue(float64(i) * 0.2), } } directory := test.NewTemporaryDirectory("test_storage", t) defer directory.Close() o := &MemorySeriesStorageOptions{ MemoryChunks: 50, PersistenceRetentionPeriod: 24 * 7 * time.Hour, PersistenceStoragePath: directory.Path(), CheckpointInterval: 250 * time.Millisecond, } storage, err := NewMemorySeriesStorage(o) if err != nil { t.Fatalf("Error creating storage: %s", err) } storage.Start() storage.AppendSamples(samples) time.Sleep(time.Second) storage.Stop() }
// timestampAtIndex implements chunkIterator. func (it *deltaEncodedChunkIterator) timestampAtIndex(idx int) clientmodel.Timestamp { offset := deltaHeaderBytes + idx*int(it.tBytes+it.vBytes) switch it.tBytes { case d1: return it.baseT + clientmodel.Timestamp(uint8(it.c[offset])) case d2: return it.baseT + clientmodel.Timestamp(binary.LittleEndian.Uint16(it.c[offset:])) case d4: return it.baseT + clientmodel.Timestamp(binary.LittleEndian.Uint32(it.c[offset:])) case d8: // Take absolute value for d8. return clientmodel.Timestamp(binary.LittleEndian.Uint64(it.c[offset:])) default: panic("Invalid number of bytes for time delta") } }
// timestampAtIndex implements chunkIterator. func (it *doubleDeltaEncodedChunkIterator) timestampAtIndex(idx int) clientmodel.Timestamp { if idx == 0 { return it.baseT } if idx == 1 { // If time bytes are at d8, the time is saved directly rather // than as a difference. if it.tBytes == d8 { return it.baseΔT } return it.baseT + it.baseΔT } offset := doubleDeltaHeaderBytes + (idx-2)*int(it.tBytes+it.vBytes) switch it.tBytes { case d1: return it.baseT + clientmodel.Timestamp(idx)*it.baseΔT + clientmodel.Timestamp(int8(it.c[offset])) case d2: return it.baseT + clientmodel.Timestamp(idx)*it.baseΔT + clientmodel.Timestamp(int16(binary.LittleEndian.Uint16(it.c[offset:]))) case d4: return it.baseT + clientmodel.Timestamp(idx)*it.baseΔT + clientmodel.Timestamp(int32(binary.LittleEndian.Uint32(it.c[offset:]))) case d8: // Take absolute value for d8. return clientmodel.Timestamp(binary.LittleEndian.Uint64(it.c[offset:])) default: panic("Invalid number of bytes for time delta") } }
// loadChunkDescs loads the chunkDescs for a series from disk. offsetFromEnd is // the number of chunkDescs to skip from the end of the series file. It is the // caller's responsibility to not persist or drop anything for the same // fingerprint concurrently. func (p *persistence) loadChunkDescs(fp clientmodel.Fingerprint, offsetFromEnd int) ([]*chunkDesc, error) { f, err := p.openChunkFileForReading(fp) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } defer f.Close() fi, err := f.Stat() if err != nil { return nil, err } if fi.Size()%int64(chunkLenWithHeader) != 0 { p.setDirty(true) return nil, fmt.Errorf( "size of series file for fingerprint %v is %d, which is not a multiple of the chunk length %d", fp, fi.Size(), chunkLenWithHeader, ) } numChunks := int(fi.Size())/chunkLenWithHeader - offsetFromEnd cds := make([]*chunkDesc, numChunks) chunkTimesBuf := make([]byte, 16) for i := 0; i < numChunks; i++ { _, err := f.Seek(offsetForChunkIndex(i)+chunkHeaderFirstTimeOffset, os.SEEK_SET) if err != nil { return nil, err } _, err = io.ReadAtLeast(f, chunkTimesBuf, 16) if err != nil { return nil, err } cds[i] = &chunkDesc{ chunkFirstTime: clientmodel.Timestamp(binary.LittleEndian.Uint64(chunkTimesBuf)), chunkLastTime: clientmodel.Timestamp(binary.LittleEndian.Uint64(chunkTimesBuf[8:])), } } chunkDescOps.WithLabelValues(load).Add(float64(len(cds))) numMemChunkDescs.Add(float64(len(cds))) return cds, nil }
func (c doubleDeltaEncodedChunk) baseTimeDelta() clientmodel.Timestamp { if len(c) < doubleDeltaHeaderBaseTimeDeltaOffset+8 { return 0 } return clientmodel.Timestamp( binary.LittleEndian.Uint64( c[doubleDeltaHeaderBaseTimeDeltaOffset:], ), ) }
func randomValues(numSamples int) metric.Values { v := make(metric.Values, 0, numSamples) for i := 0; i < numSamples; i++ { v = append(v, metric.SamplePair{ Timestamp: clientmodel.Timestamp(rand.Int63()), Value: clientmodel.SampleValue(rand.NormFloat64()), }) } return v }
// TestValueAtTimeListGet tests if the timestamp is set properly in the op // retrieved from the free list and if the 'consumed' member is zeroed properly. func TestValueAtTimeListGet(t *testing.T) { l := newValueAtTimeList(1) op := l.Get(&model.Fingerprint{}, 42) op.consumed = true l.Give(op) op2 := l.Get(&model.Fingerprint{}, 4711) if op2.Consumed() { t.Error("Op retrieved from freelist is already consumed.") } if got, expected := op2.CurrentTime(), model.Timestamp(4711); got != expected { t.Errorf("op2.CurrentTime() = %d; want %d.", got, expected) } }
func TestAppendOutOfOrder(t *testing.T) { s, closer := NewTestStorage(t, 1) defer closer.Close() m := clientmodel.Metric{ clientmodel.MetricNameLabel: "out_of_order", } for i, t := range []int{0, 2, 2, 1} { s.Append(&clientmodel.Sample{ Metric: m, Timestamp: clientmodel.Timestamp(t), Value: clientmodel.SampleValue(i), }) } fp, err := s.mapper.mapFP(m.FastFingerprint(), m) if err != nil { t.Fatal(err) } pl := s.NewPreloader() defer pl.Close() err = pl.PreloadRange(fp, 0, 2, 5*time.Minute) if err != nil { t.Fatalf("Error preloading chunks: %s", err) } it := s.NewIterator(fp) want := metric.Values{ { Timestamp: 0, Value: 0, }, { Timestamp: 2, Value: 1, }, } got := it.RangeValues(metric.Interval{OldestInclusive: 0, NewestInclusive: 2}) if !reflect.DeepEqual(want, got) { t.Fatalf("want %v, got %v", want, got) } }
func buildTestChunks(encoding chunkEncoding) map[clientmodel.Fingerprint][]chunk { fps := clientmodel.Fingerprints{ m1.FastFingerprint(), m2.FastFingerprint(), m3.FastFingerprint(), } fpToChunks := map[clientmodel.Fingerprint][]chunk{} for _, fp := range fps { fpToChunks[fp] = make([]chunk, 0, 10) for i := 0; i < 10; i++ { fpToChunks[fp] = append(fpToChunks[fp], newChunkForEncoding(encoding).add(&metric.SamplePair{ Timestamp: clientmodel.Timestamp(i), Value: clientmodel.SampleValue(fp), })[0]) } } return fpToChunks }
func BenchmarkAppend(b *testing.B) { samples := make(clientmodel.Samples, b.N) for i := range samples { samples[i] = &clientmodel.Sample{ Metric: clientmodel.Metric{ clientmodel.MetricNameLabel: clientmodel.LabelValue(fmt.Sprintf("test_metric_%d", i%10)), "label1": clientmodel.LabelValue(fmt.Sprintf("test_metric_%d", i%10)), "label2": clientmodel.LabelValue(fmt.Sprintf("test_metric_%d", i%10)), }, Timestamp: clientmodel.Timestamp(i), Value: clientmodel.SampleValue(i), } } b.ResetTimer() s, closer := NewTestStorage(b) defer closer.Close() s.AppendSamples(samples) }
// preloadChunksForRange loads chunks for the given range from the persistence. // The caller must have locked the fingerprint of the series. func (s *memorySeries) preloadChunksForRange( from clientmodel.Timestamp, through clientmodel.Timestamp, fp clientmodel.Fingerprint, mss *memorySeriesStorage, ) ([]*chunkDesc, error) { firstChunkDescTime := clientmodel.Timestamp(math.MaxInt64) if len(s.chunkDescs) > 0 { firstChunkDescTime = s.chunkDescs[0].firstTime() } if s.chunkDescsOffset != 0 && from.Before(firstChunkDescTime) { cds, err := mss.loadChunkDescs(fp, firstChunkDescTime) if err != nil { return nil, err } s.chunkDescs = append(cds, s.chunkDescs...) s.chunkDescsOffset = 0 } if len(s.chunkDescs) == 0 { return nil, nil } // Find first chunk with start time after "from". fromIdx := sort.Search(len(s.chunkDescs), func(i int) bool { return s.chunkDescs[i].firstTime().After(from) }) // Find first chunk with start time after "through". throughIdx := sort.Search(len(s.chunkDescs), func(i int) bool { return s.chunkDescs[i].firstTime().After(through) }) if fromIdx > 0 { fromIdx-- } if throughIdx == len(s.chunkDescs) { throughIdx-- } pinIndexes := make([]int, 0, throughIdx-fromIdx+1) for i := fromIdx; i <= throughIdx; i++ { pinIndexes = append(pinIndexes, i) } return s.preloadChunks(pinIndexes, mss) }
// TestLoop is just a smoke test for the loop method, if we can switch it on and // off without disaster. func TestLoop(t *testing.T) { if testing.Short() { t.Skip("Skipping test in short mode.") } samples := make(clientmodel.Samples, 1000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(2 * i), Value: clientmodel.SampleValue(float64(i) * 0.2), } } directory := testutil.NewTemporaryDirectory("test_storage", t) defer directory.Close() o := &MemorySeriesStorageOptions{ MemoryChunks: 50, MaxChunksToPersist: 1000000, PersistenceRetentionPeriod: 24 * 7 * time.Hour, PersistenceStoragePath: directory.Path(), CheckpointInterval: 250 * time.Millisecond, SyncStrategy: Adaptive, } storage := NewMemorySeriesStorage(o) if err := storage.Start(); err != nil { t.Fatalf("Error starting storage: %s", err) } for _, s := range samples { storage.Append(s) } storage.WaitForIndexing() series, _ := storage.(*memorySeriesStorage).fpToSeries.get(clientmodel.Metric{}.FastFingerprint()) cdsBefore := len(series.chunkDescs) time.Sleep(fpMaxWaitDuration + time.Second) // TODO(beorn7): Ugh, need to wait for maintenance to kick in. cdsAfter := len(series.chunkDescs) storage.Stop() if cdsBefore <= cdsAfter { t.Errorf( "Number of chunk descriptors should have gone down by now. Got before %d, after %d.", cdsBefore, cdsAfter, ) } }
func testChunk(t *testing.T, encoding chunkEncoding) { samples := make(clientmodel.Samples, 500000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(i), Value: clientmodel.SampleValue(float64(i) * 0.2), } } s, closer := NewTestStorage(t, encoding) defer closer.Close() for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() for m := range s.fpToSeries.iter() { s.fpLocker.Lock(m.fp) var values metric.Values for _, cd := range m.series.chunkDescs { if cd.isEvicted() { continue } for sample := range cd.c.newIterator().values() { values = append(values, *sample) } } for i, v := range values { if samples[i].Timestamp != v.Timestamp { t.Errorf("%d. Got %v; want %v", i, v.Timestamp, samples[i].Timestamp) } if samples[i].Value != v.Value { t.Errorf("%d. Got %v; want %v", i, v.Value, samples[i].Value) } } s.fpLocker.Unlock(m.fp) } log.Info("test done, closing") }
func benchmarkRangeValues(b *testing.B, encoding chunkEncoding) { samples := make(clientmodel.Samples, 10000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(2 * i), Value: clientmodel.SampleValue(float64(i) * 0.2), } } s, closer := NewTestStorage(b, encoding) defer closer.Close() for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() fp := clientmodel.Metric{}.FastFingerprint() b.ResetTimer() for i := 0; i < b.N; i++ { it := s.NewIterator(fp) for _, sample := range samples { actual := it.RangeValues(metric.Interval{ OldestInclusive: sample.Timestamp - 20, NewestInclusive: sample.Timestamp + 20, }) if len(actual) < 10 { b.Fatalf("not enough samples found") } } } }
func testEvictAndPurgeSeries(t *testing.T, encoding chunkEncoding) { samples := make(clientmodel.Samples, 10000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(2 * i), Value: clientmodel.SampleValue(float64(i * i)), } } s, closer := NewTestStorage(t, encoding) defer closer.Close() for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() fp := clientmodel.Metric{}.FastFingerprint() // Drop ~half of the chunks. s.maintainMemorySeries(fp, 10000) it := s.NewIterator(fp) actual := it.BoundaryValues(metric.Interval{ OldestInclusive: 0, NewestInclusive: 100000, }) if len(actual) != 2 { t.Fatal("expected two results after purging half of series") } if actual[0].Timestamp < 6000 || actual[0].Timestamp > 10000 { t.Errorf("1st timestamp out of expected range: %v", actual[0].Timestamp) } want := clientmodel.Timestamp(19998) if actual[1].Timestamp != want { t.Errorf("2nd timestamp: want %v, got %v", want, actual[1].Timestamp) } // Drop everything. s.maintainMemorySeries(fp, 100000) it = s.NewIterator(fp) actual = it.BoundaryValues(metric.Interval{ OldestInclusive: 0, NewestInclusive: 100000, }) if len(actual) != 0 { t.Fatal("expected zero results after purging the whole series") } // Recreate series. for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() series, ok := s.fpToSeries.get(fp) if !ok { t.Fatal("could not find series") } // Persist head chunk so we can safely archive. series.headChunkClosed = true s.maintainMemorySeries(fp, clientmodel.Earliest) // Archive metrics. s.fpToSeries.del(fp) if err := s.persistence.archiveMetric( fp, series.metric, series.firstTime(), series.head().lastTime(), ); err != nil { t.Fatal(err) } archived, _, _, err := s.persistence.hasArchivedMetric(fp) if err != nil { t.Fatal(err) } if !archived { t.Fatal("not archived") } // Drop ~half of the chunks of an archived series. s.maintainArchivedSeries(fp, 10000) archived, _, _, err = s.persistence.hasArchivedMetric(fp) if err != nil { t.Fatal(err) } if !archived { t.Fatal("archived series purged although only half of the chunks dropped") } // Drop everything. s.maintainArchivedSeries(fp, 100000) archived, _, _, err = s.persistence.hasArchivedMetric(fp) if err != nil { t.Fatal(err) } if archived { t.Fatal("archived series not dropped") } // Recreate series. for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() series, ok = s.fpToSeries.get(fp) if !ok { t.Fatal("could not find series") } // Persist head chunk so we can safely archive. series.headChunkClosed = true s.maintainMemorySeries(fp, clientmodel.Earliest) // Archive metrics. s.fpToSeries.del(fp) if err := s.persistence.archiveMetric( fp, series.metric, series.firstTime(), series.head().lastTime(), ); err != nil { t.Fatal(err) } archived, _, _, err = s.persistence.hasArchivedMetric(fp) if err != nil { t.Fatal(err) } if !archived { t.Fatal("not archived") } // Unarchive metrics. s.getOrCreateSeries(fp, clientmodel.Metric{}) series, ok = s.fpToSeries.get(fp) if !ok { t.Fatal("could not find series") } archived, _, _, err = s.persistence.hasArchivedMetric(fp) if err != nil { t.Fatal(err) } if archived { t.Fatal("archived") } // This will archive again, but must not drop it completely, despite the // memorySeries being empty. s.maintainMemorySeries(fp, 10000) archived, _, _, err = s.persistence.hasArchivedMetric(fp) if err != nil { t.Fatal(err) } if !archived { t.Fatal("series purged completely") } }
func TestTemplateExpansion(t *testing.T) { scenarios := []testTemplatesScenario{ { // No template. text: "plain text", output: "plain text", }, { // Simple value. text: "{{ 1 }}", output: "1", }, { // HTML escaping. text: "{{ \"<b>\" }}", output: "<b>", html: true, }, { // Disabling HTML escaping. text: "{{ \"<b>\" | safeHtml }}", output: "<b>", html: true, }, { // HTML escaping doesn't apply to non-html. text: "{{ \"<b>\" }}", output: "<b>", }, { // Pass multiple arguments to templates. text: "{{define \"x\"}}{{.arg0}} {{.arg1}}{{end}}{{template \"x\" (args 1 \"2\")}}", output: "1 2", }, { text: "{{ query \"1.5\" | first | value }}", output: "1.5", }, { // Get value from scalar query. text: "{{ query \"scalar(count(metric))\" | first | value }}", output: "2", }, { // Get value from query. text: "{{ query \"metric{instance='a'}\" | first | value }}", output: "11", }, { // Get label from query. text: "{{ query \"metric{instance='a'}\" | first | label \"instance\" }}", output: "a", }, { // Range over query and sort by label. text: "{{ range query \"metric\" | sortByLabel \"instance\" }}{{.Labels.instance}}:{{.Value}}: {{end}}", output: "a:11: b:21: ", }, { // Unparsable template. text: "{{", shouldFail: true, }, { // Error in function. text: "{{ query \"missing\" | first }}", shouldFail: true, }, { // Panic. text: "{{ (query \"missing\").banana }}", shouldFail: true, }, { // Regex replacement. text: "{{ reReplaceAll \"(a)b\" \"x$1\" \"ab\" }}", output: "xa", }, { // Humanize. text: "{{ range . }}{{ humanize . }}:{{ end }}", input: []float64{0.0, 1.0, 1234567.0, .12}, output: "0:1:1.235M:120m:", }, { // Humanize1024. text: "{{ range . }}{{ humanize1024 . }}:{{ end }}", input: []float64{0.0, 1.0, 1048576.0, .12}, output: "0:1:1Mi:0.12:", }, { // HumanizeDuration - seconds. text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}", input: []float64{0, 1, 60, 3600, 86400, 86400 + 3600, -(86400*2 + 3600*3 + 60*4 + 5), 899.99}, output: "0s:1s:1m 0s:1h 0m 0s:1d 0h 0m 0s:1d 1h 0m 0s:-2d 3h 4m 5s:14m 59s:", }, { // HumanizeDuration - subsecond and fractional seconds. text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}", input: []float64{.1, .0001, .12345, 60.1, 60.5, 1.2345, 12.345}, output: "100ms:100us:123.5ms:1m 0s:1m 0s:1.234s:12.35s:", }, { // Humanize* Inf and NaN. text: "{{ range . }}{{ humanize . }}:{{ humanize1024 . }}:{{ humanizeDuration . }}:{{humanizeTimestamp .}}:{{ end }}", input: []float64{math.Inf(1), math.Inf(-1), math.NaN()}, output: "+Inf:+Inf:+Inf:+Inf:-Inf:-Inf:-Inf:-Inf:NaN:NaN:NaN:NaN:", }, { // HumanizeTimestamp - clientmodel.SampleValue input. text: "{{ 1435065584.128 | humanizeTimestamp }}", output: "2015-06-23 13:19:44.128 +0000 UTC", }, { // Title. text: "{{ \"aa bb CC\" | title }}", output: "Aa Bb CC", }, { // Match. text: "{{ match \"a+\" \"aa\" }} {{ match \"a+\" \"b\" }}", output: "true false", }, { // graphLink. text: "{{ graphLink \"up\" }}", output: "/graph#%5B%7B%22expr%22%3A%22up%22%2C%22tab%22%3A0%7D%5D", }, { // tableLink. text: "{{ tableLink \"up\" }}", output: "/graph#%5B%7B%22expr%22%3A%22up%22%2C%22tab%22%3A1%7D%5D", }, { // tmpl. text: "{{ define \"a\" }}x{{ end }}{{ $name := \"a\"}}{{ tmpl $name . }}", output: "x", html: true, }, } time := clientmodel.Timestamp(0) storage, closer := local.NewTestStorage(t, 1) defer closer.Close() storage.Append(&clientmodel.Sample{ Metric: clientmodel.Metric{ clientmodel.MetricNameLabel: "metric", "instance": "a"}, Value: 11, }) storage.Append(&clientmodel.Sample{ Metric: clientmodel.Metric{ clientmodel.MetricNameLabel: "metric", "instance": "b"}, Value: 21, }) storage.WaitForIndexing() engine := promql.NewEngine(storage, nil) for i, s := range scenarios { var result string var err error expander := NewTemplateExpander(s.text, "test", s.input, time, engine, "") if s.html { result, err = expander.ExpandHTML(nil) } else { result, err = expander.Expand() } if s.shouldFail { if err == nil { t.Fatalf("%d. Error not returned from %v", i, s.text) } continue } if err != nil { t.Fatalf("%d. Error returned from %v: %v", i, s.text, err) continue } if result != s.output { t.Fatalf("%d. Error in result from %v: Expected '%v' Got '%v'", i, s.text, s.output, result) continue } } }
import ( "fmt" "strings" "testing" "time" clientmodel "github.com/prometheus/client_golang/model" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/storage/local" "github.com/prometheus/prometheus/storage/metric" ) var ( testSampleInterval = time.Duration(5) * time.Minute testStartTime = clientmodel.Timestamp(0) ) func getTestValueStream(startVal clientmodel.SampleValue, endVal clientmodel.SampleValue, stepVal clientmodel.SampleValue, startTime clientmodel.Timestamp) (resultValues metric.Values) { currentTime := startTime for currentVal := startVal; currentVal <= endVal; currentVal += stepVal { sample := metric.SamplePair{ Value: currentVal, Timestamp: currentTime, } resultValues = append(resultValues, sample) currentTime = currentTime.Add(testSampleInterval) } return resultValues }
func testValueAtTime(t *testing.T, encoding chunkEncoding) { samples := make(clientmodel.Samples, 10000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(2 * i), Value: clientmodel.SampleValue(float64(i) * 0.2), } } s, closer := NewTestStorage(t, encoding) defer closer.Close() for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() fp := clientmodel.Metric{}.FastFingerprint() it := s.NewIterator(fp) // #1 Exactly on a sample. for i, expected := range samples { actual := it.ValueAtTime(expected.Timestamp) if len(actual) != 1 { t.Fatalf("1.%d. Expected exactly one result, got %d.", i, len(actual)) } if expected.Timestamp != actual[0].Timestamp { t.Errorf("1.%d. Got %v; want %v", i, actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { t.Errorf("1.%d. Got %v; want %v", i, actual[0].Value, expected.Value) } } // #2 Between samples. for i, expected1 := range samples { if i == len(samples)-1 { continue } expected2 := samples[i+1] actual := it.ValueAtTime(expected1.Timestamp + 1) if len(actual) != 2 { t.Fatalf("2.%d. Expected exactly 2 results, got %d.", i, len(actual)) } if expected1.Timestamp != actual[0].Timestamp { t.Errorf("2.%d. Got %v; want %v", i, actual[0].Timestamp, expected1.Timestamp) } if expected1.Value != actual[0].Value { t.Errorf("2.%d. Got %v; want %v", i, actual[0].Value, expected1.Value) } if expected2.Timestamp != actual[1].Timestamp { t.Errorf("2.%d. Got %v; want %v", i, actual[1].Timestamp, expected1.Timestamp) } if expected2.Value != actual[1].Value { t.Errorf("2.%d. Got %v; want %v", i, actual[1].Value, expected1.Value) } } // #3 Corner cases: Just before the first sample, just after the last. expected := samples[0] actual := it.ValueAtTime(expected.Timestamp - 1) if len(actual) != 1 { t.Fatalf("3.1. Expected exactly one result, got %d.", len(actual)) } if expected.Timestamp != actual[0].Timestamp { t.Errorf("3.1. Got %v; want %v", actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { t.Errorf("3.1. Got %v; want %v", actual[0].Value, expected.Value) } expected = samples[len(samples)-1] actual = it.ValueAtTime(expected.Timestamp + 1) if len(actual) != 1 { t.Fatalf("3.2. Expected exactly one result, got %d.", len(actual)) } if expected.Timestamp != actual[0].Timestamp { t.Errorf("3.2. Got %v; want %v", actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { t.Errorf("3.2. Got %v; want %v", actual[0].Value, expected.Value) } }
func TestEndpoints(t *testing.T) { suite, err := promql.NewTest(t, ` load 1m test_metric1{foo="bar"} 0+100x100 test_metric1{foo="boo"} 1+0x100 test_metric2{foo="boo"} 1+0x100 `) if err != nil { t.Fatal(err) } defer suite.Close() if err := suite.Run(); err != nil { t.Fatal(err) } api := &API{ Storage: suite.Storage(), QueryEngine: suite.QueryEngine(), } start := clientmodel.Timestamp(0) var tests = []struct { endpoint apiFunc params map[string]string query url.Values response interface{} errType errorType }{ { endpoint: api.query, query: url.Values{ "query": []string{"2"}, "time": []string{"123.3"}, }, response: &queryData{ ResultType: promql.ExprScalar, Result: &promql.Scalar{ Value: 2, Timestamp: start.Add(123*time.Second + 300*time.Millisecond), }, }, }, { endpoint: api.query, query: url.Values{ "query": []string{"0.333"}, "time": []string{"1970-01-01T00:02:03Z"}, }, response: &queryData{ ResultType: promql.ExprScalar, Result: &promql.Scalar{ Value: 0.333, Timestamp: start.Add(123 * time.Second), }, }, }, { endpoint: api.query, query: url.Values{ "query": []string{"0.333"}, "time": []string{"1970-01-01T01:02:03+01:00"}, }, response: &queryData{ ResultType: promql.ExprScalar, Result: &promql.Scalar{ Value: 0.333, Timestamp: start.Add(123 * time.Second), }, }, }, { endpoint: api.queryRange, query: url.Values{ "query": []string{"time()"}, "start": []string{"0"}, "end": []string{"2"}, "step": []string{"1"}, }, response: &queryData{ ResultType: promql.ExprMatrix, Result: promql.Matrix{ &promql.SampleStream{ Values: metric.Values{ {Value: 0, Timestamp: start}, {Value: 1, Timestamp: start.Add(1 * time.Second)}, {Value: 2, Timestamp: start.Add(2 * time.Second)}, }, }, }, }, }, // Missing query params in range queries. { endpoint: api.queryRange, query: url.Values{ "query": []string{"time()"}, "end": []string{"2"}, "step": []string{"1"}, }, errType: errorBadData, }, { endpoint: api.queryRange, query: url.Values{ "query": []string{"time()"}, "start": []string{"0"}, "step": []string{"1"}, }, errType: errorBadData, }, { endpoint: api.queryRange, query: url.Values{ "query": []string{"time()"}, "start": []string{"0"}, "end": []string{"2"}, }, errType: errorBadData, }, // Missing evaluation time. { endpoint: api.query, query: url.Values{ "query": []string{"0.333"}, }, errType: errorBadData, }, // Bad query expression. { endpoint: api.query, query: url.Values{ "query": []string{"invalid][query"}, "time": []string{"1970-01-01T01:02:03+01:00"}, }, errType: errorBadData, }, { endpoint: api.queryRange, query: url.Values{ "query": []string{"invalid][query"}, "start": []string{"0"}, "end": []string{"100"}, "step": []string{"1"}, }, errType: errorBadData, }, { endpoint: api.labelValues, params: map[string]string{ "name": "__name__", }, response: clientmodel.LabelValues{ "test_metric1", "test_metric2", }, }, { endpoint: api.labelValues, params: map[string]string{ "name": "foo", }, response: clientmodel.LabelValues{ "bar", "boo", }, }, // Bad name parameter. { endpoint: api.labelValues, params: map[string]string{ "name": "not!!!allowed", }, errType: errorBadData, }, { endpoint: api.series, query: url.Values{ "match[]": []string{`test_metric2`}, }, response: []clientmodel.Metric{ { "__name__": "test_metric2", "foo": "boo", }, }, }, { endpoint: api.series, query: url.Values{ "match[]": []string{`test_metric1{foo=~"o$"}`}, }, response: []clientmodel.Metric{ { "__name__": "test_metric1", "foo": "boo", }, }, }, { endpoint: api.series, query: url.Values{ "match[]": []string{`test_metric1{foo=~"o$"}`, `test_metric1{foo=~"o$"}`}, }, response: []clientmodel.Metric{ { "__name__": "test_metric1", "foo": "boo", }, }, }, { endpoint: api.series, query: url.Values{ "match[]": []string{`test_metric1{foo=~"o$"}`, `none`}, }, response: []clientmodel.Metric{ { "__name__": "test_metric1", "foo": "boo", }, }, }, // Missing match[] query params in series requests. { endpoint: api.series, errType: errorBadData, }, { endpoint: api.dropSeries, errType: errorBadData, }, // The following tests delete time series from the test storage. They // must remain at the end and are fixed in their order. { endpoint: api.dropSeries, query: url.Values{ "match[]": []string{`test_metric1{foo=~"o$"}`}, }, response: struct { NumDeleted int `json:"numDeleted"` }{1}, }, { endpoint: api.series, query: url.Values{ "match[]": []string{`test_metric1`}, }, response: []clientmodel.Metric{ { "__name__": "test_metric1", "foo": "bar", }, }, }, { endpoint: api.dropSeries, query: url.Values{ "match[]": []string{`{__name__=~".*"}`}, }, response: struct { NumDeleted int `json:"numDeleted"` }{2}, }, } for _, test := range tests { // Build a context with the correct request params. ctx := context.Background() for p, v := range test.params { ctx = route.WithParam(ctx, p, v) } api.context = func(r *http.Request) context.Context { return ctx } req, err := http.NewRequest("ANY", fmt.Sprintf("http://example.com?%s", test.query.Encode()), nil) if err != nil { t.Fatal(err) } resp, apiErr := test.endpoint(req) if apiErr != nil { if test.errType == errorNone { t.Fatalf("Unexpected error: %s", apiErr) } if test.errType != apiErr.typ { t.Fatalf("Expected error of type %q but got type %q", test.errType, apiErr.typ) } continue } if apiErr == nil && test.errType != errorNone { t.Fatalf("Expected error of type %q but got none", test.errType) } if !reflect.DeepEqual(resp, test.response) { t.Fatalf("Response does not match, expected:\n%#v\ngot:\n%#v", test.response, resp) } // Ensure that removed metrics are unindexed before the next request. suite.Storage().WaitForIndexing() } }
// dropAndPersistChunks deletes all chunks from a series file whose last sample // time is before beforeTime, and then appends the provided chunks, leaving out // those whose last sample time is before beforeTime. It returns the timestamp // of the first sample in the oldest chunk _not_ dropped, the offset within the // series file of the first chunk persisted (out of the provided chunks), the // number of deleted chunks, and true if all chunks of the series have been // deleted (in which case the returned timestamp will be 0 and must be ignored). // It is the caller's responsibility to make sure nothing is persisted or loaded // for the same fingerprint concurrently. func (p *persistence) dropAndPersistChunks( fp clientmodel.Fingerprint, beforeTime clientmodel.Timestamp, chunks []chunk, ) ( firstTimeNotDropped clientmodel.Timestamp, offset int, numDropped int, allDropped bool, err error, ) { // Style note: With the many return values, it was decided to use naked // returns in this method. They make the method more readable, but // please handle with care! defer func() { if err != nil { log.Error("Error dropping and/or persisting chunks: ", err) p.setDirty(true) } }() if len(chunks) > 0 { // We have chunks to persist. First check if those are already // too old. If that's the case, the chunks in the series file // are all too old, too. i := 0 for ; i < len(chunks) && chunks[i].newIterator().lastTimestamp().Before(beforeTime); i++ { } if i < len(chunks) { firstTimeNotDropped = chunks[i].firstTime() } if i > 0 || firstTimeNotDropped.Before(beforeTime) { // Series file has to go. if numDropped, err = p.deleteSeriesFile(fp); err != nil { return } numDropped += i if i == len(chunks) { allDropped = true return } // Now simply persist what has to be persisted to a new file. _, err = p.persistChunks(fp, chunks[i:]) return } } // If we are here, we have to check the series file itself. f, err := p.openChunkFileForReading(fp) if os.IsNotExist(err) { // No series file. Only need to create new file with chunks to // persist, if there are any. if len(chunks) == 0 { allDropped = true err = nil // Do not report not-exist err. return } offset, err = p.persistChunks(fp, chunks) return } if err != nil { return } defer f.Close() // Find the first chunk in the file that should be kept. for ; ; numDropped++ { _, err = f.Seek(offsetForChunkIndex(numDropped), os.SEEK_SET) if err != nil { return } headerBuf := make([]byte, chunkHeaderLen) _, err = io.ReadFull(f, headerBuf) if err == io.EOF { // We ran into the end of the file without finding any chunks that should // be kept. Remove the whole file. if numDropped, err = p.deleteSeriesFile(fp); err != nil { return } if len(chunks) == 0 { allDropped = true return } offset, err = p.persistChunks(fp, chunks) return } if err != nil { return } lastTime := clientmodel.Timestamp( binary.LittleEndian.Uint64(headerBuf[chunkHeaderLastTimeOffset:]), ) if !lastTime.Before(beforeTime) { firstTimeNotDropped = clientmodel.Timestamp( binary.LittleEndian.Uint64(headerBuf[chunkHeaderFirstTimeOffset:]), ) chunkOps.WithLabelValues(drop).Add(float64(numDropped)) break } } // We've found the first chunk that should be kept. If it is the first // one, just append the chunks. if numDropped == 0 { if len(chunks) > 0 { offset, err = p.persistChunks(fp, chunks) } return } // Otherwise, seek backwards to the beginning of its header and start // copying everything from there into a new file. Then append the chunks // to the new file. _, err = f.Seek(-chunkHeaderLen, os.SEEK_CUR) if err != nil { return } temp, err := os.OpenFile(p.tempFileNameForFingerprint(fp), os.O_WRONLY|os.O_CREATE, 0640) if err != nil { return } defer func() { p.closeChunkFile(temp) if err == nil { err = os.Rename(p.tempFileNameForFingerprint(fp), p.fileNameForFingerprint(fp)) } }() written, err := io.Copy(temp, f) if err != nil { return } offset = int(written / chunkLenWithHeader) if len(chunks) > 0 { if err = writeChunks(temp, chunks); err != nil { return } } return }
// loadSeriesMapAndHeads loads the fingerprint to memory-series mapping and all // the chunks contained in the checkpoint (and thus not yet persisted to series // files). The method is capable of loading the checkpoint format v1 and v2. If // recoverable corruption is detected, or if the dirty flag was set from the // beginning, crash recovery is run, which might take a while. If an // unrecoverable error is encountered, it is returned. Call this method during // start-up while nothing else is running in storage land. This method is // utterly goroutine-unsafe. func (p *persistence) loadSeriesMapAndHeads() (sm *seriesMap, chunksToPersist int64, err error) { var chunkDescsTotal int64 fingerprintToSeries := make(map[clientmodel.Fingerprint]*memorySeries) sm = &seriesMap{m: fingerprintToSeries} defer func() { if sm != nil && p.dirty { log.Warn("Persistence layer appears dirty.") err = p.recoverFromCrash(fingerprintToSeries) if err != nil { sm = nil } } if err == nil { numMemChunkDescs.Add(float64(chunkDescsTotal)) } }() f, err := os.Open(p.headsFileName()) if os.IsNotExist(err) { return sm, 0, nil } if err != nil { log.Warn("Could not open heads file:", err) p.dirty = true return } defer f.Close() r := bufio.NewReaderSize(f, fileBufSize) buf := make([]byte, len(headsMagicString)) if _, err := io.ReadFull(r, buf); err != nil { log.Warn("Could not read from heads file:", err) p.dirty = true return sm, 0, nil } magic := string(buf) if magic != headsMagicString { log.Warnf( "unexpected magic string, want %q, got %q", headsMagicString, magic, ) p.dirty = true return } version, err := binary.ReadVarint(r) if (version != headsFormatVersion && version != headsFormatLegacyVersion) || err != nil { log.Warnf("unknown heads format version, want %d", headsFormatVersion) p.dirty = true return sm, 0, nil } numSeries, err := codable.DecodeUint64(r) if err != nil { log.Warn("Could not decode number of series:", err) p.dirty = true return sm, 0, nil } for ; numSeries > 0; numSeries-- { seriesFlags, err := r.ReadByte() if err != nil { log.Warn("Could not read series flags:", err) p.dirty = true return sm, chunksToPersist, nil } headChunkPersisted := seriesFlags&flagHeadChunkPersisted != 0 fp, err := codable.DecodeUint64(r) if err != nil { log.Warn("Could not decode fingerprint:", err) p.dirty = true return sm, chunksToPersist, nil } var metric codable.Metric if err := metric.UnmarshalFromReader(r); err != nil { log.Warn("Could not decode metric:", err) p.dirty = true return sm, chunksToPersist, nil } var persistWatermark int64 var modTime time.Time if version != headsFormatLegacyVersion { // persistWatermark only present in v2. persistWatermark, err = binary.ReadVarint(r) if err != nil { log.Warn("Could not decode persist watermark:", err) p.dirty = true return sm, chunksToPersist, nil } modTimeNano, err := binary.ReadVarint(r) if err != nil { log.Warn("Could not decode modification time:", err) p.dirty = true return sm, chunksToPersist, nil } if modTimeNano != -1 { modTime = time.Unix(0, modTimeNano) } } chunkDescsOffset, err := binary.ReadVarint(r) if err != nil { log.Warn("Could not decode chunk descriptor offset:", err) p.dirty = true return sm, chunksToPersist, nil } savedFirstTime, err := binary.ReadVarint(r) if err != nil { log.Warn("Could not decode saved first time:", err) p.dirty = true return sm, chunksToPersist, nil } numChunkDescs, err := binary.ReadVarint(r) if err != nil { log.Warn("Could not decode number of chunk descriptors:", err) p.dirty = true return sm, chunksToPersist, nil } chunkDescs := make([]*chunkDesc, numChunkDescs) if version == headsFormatLegacyVersion { if headChunkPersisted { persistWatermark = numChunkDescs } else { persistWatermark = numChunkDescs - 1 } } for i := int64(0); i < numChunkDescs; i++ { if i < persistWatermark { firstTime, err := binary.ReadVarint(r) if err != nil { log.Warn("Could not decode first time:", err) p.dirty = true return sm, chunksToPersist, nil } lastTime, err := binary.ReadVarint(r) if err != nil { log.Warn("Could not decode last time:", err) p.dirty = true return sm, chunksToPersist, nil } chunkDescs[i] = &chunkDesc{ chunkFirstTime: clientmodel.Timestamp(firstTime), chunkLastTime: clientmodel.Timestamp(lastTime), } chunkDescsTotal++ } else { // Non-persisted chunk. encoding, err := r.ReadByte() if err != nil { log.Warn("Could not decode chunk type:", err) p.dirty = true return sm, chunksToPersist, nil } chunk := newChunkForEncoding(chunkEncoding(encoding)) if err := chunk.unmarshal(r); err != nil { log.Warn("Could not decode chunk:", err) p.dirty = true return sm, chunksToPersist, nil } chunkDescs[i] = newChunkDesc(chunk) chunksToPersist++ } } fingerprintToSeries[clientmodel.Fingerprint(fp)] = &memorySeries{ metric: clientmodel.Metric(metric), chunkDescs: chunkDescs, persistWatermark: int(persistWatermark), modTime: modTime, chunkDescsOffset: int(chunkDescsOffset), savedFirstTime: clientmodel.Timestamp(savedFirstTime), headChunkClosed: persistWatermark >= numChunkDescs, } } return sm, chunksToPersist, nil }
func benchmarkValueAtTime(b *testing.B, encoding chunkEncoding) { samples := make(clientmodel.Samples, 10000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(2 * i), Value: clientmodel.SampleValue(float64(i) * 0.2), } } s, closer := NewTestStorage(b, encoding) defer closer.Close() for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() fp := clientmodel.Metric{}.FastFingerprint() b.ResetTimer() for i := 0; i < b.N; i++ { it := s.NewIterator(fp) // #1 Exactly on a sample. for i, expected := range samples { actual := it.ValueAtTime(expected.Timestamp) if len(actual) != 1 { b.Fatalf("1.%d. Expected exactly one result, got %d.", i, len(actual)) } if expected.Timestamp != actual[0].Timestamp { b.Errorf("1.%d. Got %v; want %v", i, actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { b.Errorf("1.%d. Got %v; want %v", i, actual[0].Value, expected.Value) } } // #2 Between samples. for i, expected1 := range samples { if i == len(samples)-1 { continue } expected2 := samples[i+1] actual := it.ValueAtTime(expected1.Timestamp + 1) if len(actual) != 2 { b.Fatalf("2.%d. Expected exactly 2 results, got %d.", i, len(actual)) } if expected1.Timestamp != actual[0].Timestamp { b.Errorf("2.%d. Got %v; want %v", i, actual[0].Timestamp, expected1.Timestamp) } if expected1.Value != actual[0].Value { b.Errorf("2.%d. Got %v; want %v", i, actual[0].Value, expected1.Value) } if expected2.Timestamp != actual[1].Timestamp { b.Errorf("2.%d. Got %v; want %v", i, actual[1].Timestamp, expected1.Timestamp) } if expected2.Value != actual[1].Value { b.Errorf("2.%d. Got %v; want %v", i, actual[1].Value, expected1.Value) } } } }
func TestClient(t *testing.T) { samples := clientmodel.Samples{ { Metric: clientmodel.Metric{ clientmodel.MetricNameLabel: "testmetric", "test_label": "test_label_value1", }, Timestamp: clientmodel.Timestamp(123456789123), Value: 1.23, }, { Metric: clientmodel.Metric{ clientmodel.MetricNameLabel: "testmetric", "test_label": "test_label_value2", }, Timestamp: clientmodel.Timestamp(123456789123), Value: 5.1234, }, { Metric: clientmodel.Metric{ clientmodel.MetricNameLabel: "special_float_value", }, Timestamp: clientmodel.Timestamp(123456789123), Value: clientmodel.SampleValue(math.NaN()), }, } expectedJSON := `{"database":"prometheus","retentionPolicy":"default","points":[{"timestamp":123456789123000000,"precision":"n","name":"testmetric","tags":{"test_label":"test_label_value1"},"fields":{"value":"1.23"}},{"timestamp":123456789123000000,"precision":"n","name":"testmetric","tags":{"test_label":"test_label_value2"},"fields":{"value":"5.1234"}}]}` server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { t.Fatalf("Unexpected method; expected POST, got %s", r.Method) } if r.URL.Path != writeEndpoint { t.Fatalf("Unexpected path; expected %s, got %s", writeEndpoint, r.URL.Path) } ct := r.Header["Content-Type"] if len(ct) != 1 { t.Fatalf("Unexpected number of 'Content-Type' headers; got %d, want 1", len(ct)) } if ct[0] != contentTypeJSON { t.Fatalf("Unexpected 'Content-type'; expected %s, got %s", contentTypeJSON, ct[0]) } b, err := ioutil.ReadAll(r.Body) if err != nil { t.Fatalf("Error reading body: %s", err) } if string(b) != expectedJSON { t.Fatalf("Unexpected request body; expected:\n\n%s\n\ngot:\n\n%s", expectedJSON, string(b)) } }, )) defer server.Close() c := NewClient(server.URL, time.Minute, "prometheus", "default") if err := c.Store(samples); err != nil { t.Fatalf("Error sending samples: %s", err) } }
// add implements chunk. func (c doubleDeltaEncodedChunk) add(s *metric.SamplePair) []chunk { if c.len() == 0 { return c.addFirstSample(s) } tb := c.timeBytes() vb := c.valueBytes() if c.len() == 1 { return c.addSecondSample(s, tb, vb) } remainingBytes := cap(c) - len(c) sampleSize := c.sampleSize() // Do we generally have space for another sample in this chunk? If not, // overflow into a new one. if remainingBytes < sampleSize { overflowChunks := newChunk().add(s) return []chunk{&c, overflowChunks[0]} } projectedTime := c.baseTime() + clientmodel.Timestamp(c.len())*c.baseTimeDelta() ddt := s.Timestamp - projectedTime projectedValue := c.baseValue() + clientmodel.SampleValue(c.len())*c.baseValueDelta() ddv := s.Value - projectedValue ntb, nvb, nInt := tb, vb, c.isInt() // If the new sample is incompatible with the current encoding, reencode the // existing chunk data into new chunk(s). if c.isInt() && !isInt64(ddv) { // int->float. nvb = d4 nInt = false } else if !c.isInt() && vb == d4 && projectedValue+clientmodel.SampleValue(float32(ddv)) != s.Value { // float32->float64. nvb = d8 } else { if tb < d8 { // Maybe more bytes for timestamp. ntb = max(tb, bytesNeededForSignedTimestampDelta(ddt)) } if c.isInt() && vb < d8 { // Maybe more bytes for sample value. nvb = max(vb, bytesNeededForIntegerSampleValueDelta(ddv)) } } if tb != ntb || vb != nvb || c.isInt() != nInt { if len(c)*2 < cap(c) { return transcodeAndAdd(newDoubleDeltaEncodedChunk(ntb, nvb, nInt, cap(c)), &c, s) } // Chunk is already half full. Better create a new one and save the transcoding efforts. overflowChunks := newChunk().add(s) return []chunk{&c, overflowChunks[0]} } offset := len(c) c = c[:offset+sampleSize] switch tb { case d1: c[offset] = byte(ddt) case d2: binary.LittleEndian.PutUint16(c[offset:], uint16(ddt)) case d4: binary.LittleEndian.PutUint32(c[offset:], uint32(ddt)) case d8: // Store the absolute value (no delta) in case of d8. binary.LittleEndian.PutUint64(c[offset:], uint64(s.Timestamp)) default: panic("invalid number of bytes for time delta") } offset += int(tb) if c.isInt() { switch vb { case d0: // No-op. Constant delta is stored as base value. case d1: c[offset] = byte(int8(ddv)) case d2: binary.LittleEndian.PutUint16(c[offset:], uint16(int16(ddv))) case d4: binary.LittleEndian.PutUint32(c[offset:], uint32(int32(ddv))) // d8 must not happen. Those samples are encoded as float64. default: panic("invalid number of bytes for integer delta") } } else { switch vb { case d4: binary.LittleEndian.PutUint32(c[offset:], math.Float32bits(float32(ddv))) case d8: // Store the absolute value (no delta) in case of d8. binary.LittleEndian.PutUint64(c[offset:], math.Float64bits(float64(s.Value))) default: panic("invalid number of bytes for floating point delta") } } return []chunk{&c} }
func testRangeValues(t *testing.T, encoding chunkEncoding) { samples := make(clientmodel.Samples, 10000) for i := range samples { samples[i] = &clientmodel.Sample{ Timestamp: clientmodel.Timestamp(2 * i), Value: clientmodel.SampleValue(float64(i) * 0.2), } } s, closer := NewTestStorage(t, encoding) defer closer.Close() for _, sample := range samples { s.Append(sample) } s.WaitForIndexing() fp := clientmodel.Metric{}.FastFingerprint() it := s.NewIterator(fp) // #1 Zero length interval at sample. for i, expected := range samples { actual := it.RangeValues(metric.Interval{ OldestInclusive: expected.Timestamp, NewestInclusive: expected.Timestamp, }) if len(actual) != 1 { t.Fatalf("1.%d. Expected exactly one result, got %d.", i, len(actual)) } if expected.Timestamp != actual[0].Timestamp { t.Errorf("1.%d. Got %v; want %v.", i, actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { t.Errorf("1.%d. Got %v; want %v.", i, actual[0].Value, expected.Value) } } // #2 Zero length interval off sample. for i, expected := range samples { actual := it.RangeValues(metric.Interval{ OldestInclusive: expected.Timestamp + 1, NewestInclusive: expected.Timestamp + 1, }) if len(actual) != 0 { t.Fatalf("2.%d. Expected no result, got %d.", i, len(actual)) } } // #3 2sec interval around sample. for i, expected := range samples { actual := it.RangeValues(metric.Interval{ OldestInclusive: expected.Timestamp - 1, NewestInclusive: expected.Timestamp + 1, }) if len(actual) != 1 { t.Fatalf("3.%d. Expected exactly one result, got %d.", i, len(actual)) } if expected.Timestamp != actual[0].Timestamp { t.Errorf("3.%d. Got %v; want %v.", i, actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { t.Errorf("3.%d. Got %v; want %v.", i, actual[0].Value, expected.Value) } } // #4 2sec interval sample to sample. for i, expected1 := range samples { if i == len(samples)-1 { continue } expected2 := samples[i+1] actual := it.RangeValues(metric.Interval{ OldestInclusive: expected1.Timestamp, NewestInclusive: expected1.Timestamp + 2, }) if len(actual) != 2 { t.Fatalf("4.%d. Expected exactly 2 results, got %d.", i, len(actual)) } if expected1.Timestamp != actual[0].Timestamp { t.Errorf("4.%d. Got %v for 1st result; want %v.", i, actual[0].Timestamp, expected1.Timestamp) } if expected1.Value != actual[0].Value { t.Errorf("4.%d. Got %v for 1st result; want %v.", i, actual[0].Value, expected1.Value) } if expected2.Timestamp != actual[1].Timestamp { t.Errorf("4.%d. Got %v for 2nd result; want %v.", i, actual[1].Timestamp, expected2.Timestamp) } if expected2.Value != actual[1].Value { t.Errorf("4.%d. Got %v for 2nd result; want %v.", i, actual[1].Value, expected2.Value) } } // #5 corner cases: Interval ends at first sample, interval starts // at last sample, interval entirely before/after samples. expected := samples[0] actual := it.RangeValues(metric.Interval{ OldestInclusive: expected.Timestamp - 2, NewestInclusive: expected.Timestamp, }) if len(actual) != 1 { t.Fatalf("5.1. Expected exactly one result, got %d.", len(actual)) } if expected.Timestamp != actual[0].Timestamp { t.Errorf("5.1. Got %v; want %v.", actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { t.Errorf("5.1. Got %v; want %v.", actual[0].Value, expected.Value) } expected = samples[len(samples)-1] actual = it.RangeValues(metric.Interval{ OldestInclusive: expected.Timestamp, NewestInclusive: expected.Timestamp + 2, }) if len(actual) != 1 { t.Fatalf("5.2. Expected exactly one result, got %d.", len(actual)) } if expected.Timestamp != actual[0].Timestamp { t.Errorf("5.2. Got %v; want %v.", actual[0].Timestamp, expected.Timestamp) } if expected.Value != actual[0].Value { t.Errorf("5.2. Got %v; want %v.", actual[0].Value, expected.Value) } firstSample := samples[0] actual = it.RangeValues(metric.Interval{ OldestInclusive: firstSample.Timestamp - 4, NewestInclusive: firstSample.Timestamp - 2, }) if len(actual) != 0 { t.Fatalf("5.3. Expected no results, got %d.", len(actual)) } lastSample := samples[len(samples)-1] actual = it.RangeValues(metric.Interval{ OldestInclusive: lastSample.Timestamp + 2, NewestInclusive: lastSample.Timestamp + 4, }) if len(actual) != 0 { t.Fatalf("5.3. Expected no results, got %d.", len(actual)) } }