Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
.idea/
CLAUDE.md

# Go test binaries and profiles
*.test
*.prof
18 changes: 12 additions & 6 deletions spectator/meter/age_gauge.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package meter

import (
"strconv"

"github.com/Netflix/spectator-go/v2/spectator/writer"
)

Expand All @@ -24,19 +22,27 @@ func NewAgeGauge(id *Id, writer writer.Writer) *AgeGauge {
return &AgeGauge{id, writer, "A:" + id.spectatordId + ":"}
}

// MeterId returns the meter identifier.
// NewAgeGaugeDirect generates a new age gauge directly from a name and tags,
// without allocating an *Id or copying the tags map. commonTags carries the
// registry's extraCommonTags.
func NewAgeGaugeDirect(name string, tags, commonTags map[string]string, writer writer.Writer) *AgeGauge {
return &AgeGauge{nil, writer, buildLinePrefix("A", name, tags, commonTags)}
}

// MeterId returns the meter identifier, reconstructing it from the line prefix
// if the gauge was created directly from a name and tags.
func (g *AgeGauge) MeterId() *Id {
return g.id
return resolveMeterId(g.id, g.linePrefix)
}

// Set records the current time in seconds since the epoch.
func (g *AgeGauge) Set(seconds int64) {
if seconds >= 0 {
g.writer.Write(g.linePrefix + strconv.FormatInt(seconds, 10))
g.writer.WriteInt(g.linePrefix, seconds)
}
}

// Now records the current time in epoch seconds, using a spectatord feature.
func (g *AgeGauge) Now() {
g.writer.Write(g.linePrefix + "0")
g.writer.WriteLine(g.linePrefix, "0")
}
20 changes: 13 additions & 7 deletions spectator/meter/counter.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package meter

import (
"strconv"

"github.com/Netflix/spectator-go/v2/spectator/writer"
)

Expand All @@ -24,26 +22,34 @@ func NewCounter(id *Id, writer writer.Writer) *Counter {
return &Counter{id, writer, "c:" + id.spectatordId + ":"}
}

// MeterId returns the meter identifier.
// NewCounterDirect generates a new counter directly from a name and tags,
// without allocating an *Id or copying the tags map. commonTags carries the
// registry's extraCommonTags. MeterId reconstructs an Id on demand.
func NewCounterDirect(name string, tags, commonTags map[string]string, writer writer.Writer) *Counter {
return &Counter{nil, writer, buildLinePrefix("c", name, tags, commonTags)}
}

// MeterId returns the meter identifier, reconstructing it from the line prefix
// if the counter was created directly from a name and tags.
func (c *Counter) MeterId() *Id {
return c.id
return resolveMeterId(c.id, c.linePrefix)
}

// Increment increments the counter.
func (c *Counter) Increment() {
c.writer.Write(c.linePrefix + "1")
c.writer.WriteLine(c.linePrefix, "1")
}

// Add adds an int64 delta to the current measurement.
func (c *Counter) Add(delta int64) {
if delta > 0 {
c.writer.Write(c.linePrefix + strconv.FormatInt(delta, 10))
c.writer.WriteInt(c.linePrefix, delta)
}
}

// AddFloat adds a float64 delta to the current measurement.
func (c *Counter) AddFloat(delta float64) {
if delta > 0.0 {
c.writer.Write(c.linePrefix + strconv.FormatFloat(delta, 'f', 6, 64))
c.writer.WriteFloat(c.linePrefix, delta)
}
}
16 changes: 11 additions & 5 deletions spectator/meter/dist_summary.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package meter

import (
"strconv"

"github.com/Netflix/spectator-go/v2/spectator/writer"
)

Expand All @@ -25,14 +23,22 @@ func NewDistributionSummary(id *Id, writer writer.Writer) *DistributionSummary {
return &DistributionSummary{id, writer, "d:" + id.spectatordId + ":"}
}

// MeterId returns the meter identifier.
// NewDistributionSummaryDirect generates a new distribution summary directly from
// a name and tags, without allocating an *Id or copying the tags map. commonTags
// carries the registry's extraCommonTags.
func NewDistributionSummaryDirect(name string, tags, commonTags map[string]string, writer writer.Writer) *DistributionSummary {
return &DistributionSummary{nil, writer, buildLinePrefix("d", name, tags, commonTags)}
}

// MeterId returns the meter identifier, reconstructing it from the line prefix
// if the summary was created directly from a name and tags.
func (d *DistributionSummary) MeterId() *Id {
return d.id
return resolveMeterId(d.id, d.linePrefix)
}

// Record records a value to track within the distribution.
func (d *DistributionSummary) Record(amount int64) {
if amount >= 0 {
d.writer.Write(d.linePrefix + strconv.FormatInt(amount, 10))
d.writer.WriteInt(d.linePrefix, amount)
}
}
121 changes: 121 additions & 0 deletions spectator/meter/emit_buffer_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package meter

// End-to-end emit benchmarks: meter -> real LowLatencyBuffer (backed by a
// NoopWriter so we exercise the buffer append path without socket I/O).
// Uses only the public Counter API, so the loop body is identical before and
// after the WriteLine interface extension.

import (
"runtime"
"testing"
"time"

"github.com/Netflix/spectator-go/v2/spectator/writer"
)

type emitQuietLogger struct{}

func (emitQuietLogger) Debugf(string, ...interface{}) {}
func (emitQuietLogger) Infof(string, ...interface{}) {}
func (emitQuietLogger) Errorf(string, ...interface{}) {}

// bufWriter adapts a LowLatencyBuffer to the Writer interface (its Close has no
// error return).
type bufWriter struct{ b *writer.LowLatencyBuffer }

func (w *bufWriter) Write(line string) { w.b.Write(line) }
func (w *bufWriter) WriteLine(prefix, value string) { w.b.WriteLine(prefix, value) }
func (w *bufWriter) WriteInt(prefix string, v int64) { w.b.WriteInt(prefix, v) }
func (w *bufWriter) WriteUint(prefix string, v uint64) { w.b.WriteUint(prefix, v) }
func (w *bufWriter) WriteFloat(prefix string, v float64) { w.b.WriteFloat(prefix, v) }
func (w *bufWriter) WriteBytes(line []byte) { w.b.Write(string(line)) }
func (w *bufWriter) WriteString(line string) { w.b.Write(line) }
func (w *bufWriter) Close() error { w.b.Close(); return nil }

func newBenchBuffer(b *testing.B) *bufWriter {
b.Helper()
shards := runtime.NumCPU()
bufferSize := 2 * 16 * 60 * 1024 * shards
buf := writer.NewLowLatencyBuffer(&writer.NoopWriter{}, emitQuietLogger{}, bufferSize, time.Millisecond)
w := &bufWriter{buf}
b.Cleanup(func() { w.Close() })
return w
}

var emitBenchTags = map[string]string{
"app": "gandalfAgent",
"result": "allowed",
"policy": "explicit",
}

// Cached counter (constructed once), Increment in the loop — isolates the emit path.
func BenchmarkEmitBuffer_Counter_Increment(b *testing.B) {
c := NewCounter(NewId("gandalfAgent.authzEngine.appAuthzResult", emitBenchTags), newBenchBuffer(b))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Increment()
}
}

// Cached counter, Add(delta) in the loop — value requires formatting.
func BenchmarkEmitBuffer_Counter_Add(b *testing.B) {
c := NewCounter(NewId("gandalfAgent.authzEngine.appAuthzResult", emitBenchTags), newBenchBuffer(b))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Add(int64(i) + 1)
}
}

// Cached gauge, Set(float) in the loop.
func BenchmarkEmitBuffer_Gauge_Set(b *testing.B) {
g := NewGauge(NewId("gandalfAgent.gauge", emitBenchTags), newBenchBuffer(b))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
g.Set(3.14159)
}
}

// Cached timer, Record in the loop.
func BenchmarkEmitBuffer_Timer_Record(b *testing.B) {
t := NewTimer(NewId("gandalfAgent.timer", emitBenchTags), newBenchBuffer(b))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
t.Record(time.Duration(i) * time.Millisecond)
}
}

// Cached distribution summary, Record in the loop.
func BenchmarkEmitBuffer_DistSummary_Record(b *testing.B) {
d := NewDistributionSummary(NewId("gandalfAgent.distsummary", emitBenchTags), newBenchBuffer(b))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d.Record(int64(i))
}
}

// Construct-per-call (gandalf's actual pattern) over a real buffer, via NewId
// (the WithId path) for comparison.
func BenchmarkEmitBuffer_Counter_ConstructPerCall(b *testing.B) {
w := newBenchBuffer(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
NewCounter(NewId("gandalfAgent.authzEngine.appAuthzResult", emitBenchTags), w).Increment()
}
}

// Construct-per-call via the direct (name, tags) path — what registry.Counter
// now uses. No Id, no tag-map copy.
func BenchmarkEmitBuffer_Counter_ConstructPerCall_Direct(b *testing.B) {
w := newBenchBuffer(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
NewCounterDirect("gandalfAgent.authzEngine.appAuthzResult", emitBenchTags, nil, w).Increment()
}
}
22 changes: 17 additions & 5 deletions spectator/meter/gauge.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package meter

import (
"fmt"
"strconv"

"github.com/Netflix/spectator-go/v2/spectator/writer"
"time"
Expand Down Expand Up @@ -32,18 +31,31 @@ func NewGaugeWithTTL(id *Id, writer writer.Writer, ttl time.Duration) *Gauge {
return &Gauge{id, writer, fmt.Sprintf("g,%d", int(ttl.Seconds())) + ":" + id.spectatordId + ":"}
}

// MeterId returns the meter identifier.
// NewGaugeDirect generates a new gauge directly from a name and tags, without
// allocating an *Id or copying the tags map. commonTags carries the registry's
// extraCommonTags.
func NewGaugeDirect(name string, tags, commonTags map[string]string, writer writer.Writer) *Gauge {
return &Gauge{nil, writer, buildLinePrefix("g", name, tags, commonTags)}
}

// NewGaugeDirectWithTTL generates a new gauge with a ttl directly from a name and tags.
func NewGaugeDirectWithTTL(name string, tags, commonTags map[string]string, writer writer.Writer, ttl time.Duration) *Gauge {
return &Gauge{nil, writer, buildLinePrefix(fmt.Sprintf("g,%d", int(ttl.Seconds())), name, tags, commonTags)}
}

// MeterId returns the meter identifier, reconstructing it from the line prefix
// if the gauge was created directly from a name and tags.
func (g *Gauge) MeterId() *Id {
return g.id
return resolveMeterId(g.id, g.linePrefix)
}

// Set records the current value.
func (g *Gauge) Set(value float64) {
g.writer.Write(g.linePrefix + strconv.FormatFloat(value, 'f', 6, 64))
g.writer.WriteFloat(g.linePrefix, value)
}

// SetInt records the current value as an integer, avoiding the overhead of
// float64 conversion and formatting.
func (g *Gauge) SetInt(value int64) {
g.writer.Write(g.linePrefix + strconv.FormatInt(value, 10))
g.writer.WriteInt(g.linePrefix, value)
}
Loading
Loading