mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-19 21:45:50 +02:00
Remove dogstatsd and statsd support (#591)
This commit is contained in:
@@ -1,109 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dogstatsd // import "go.opentelemetry.io/otel/exporters/metric/dogstatsd"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/api/global"
|
||||
"go.opentelemetry.io/otel/exporters/metric/internal/statsd"
|
||||
|
||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||
"go.opentelemetry.io/otel/sdk/metric/batcher/ungrouped"
|
||||
"go.opentelemetry.io/otel/sdk/metric/controller/push"
|
||||
"go.opentelemetry.io/otel/sdk/metric/selector/simple"
|
||||
)
|
||||
|
||||
type (
|
||||
Config = statsd.Config
|
||||
|
||||
// Exporter implements a dogstatsd-format statsd exporter,
|
||||
// which encodes label sets as independent fields in the
|
||||
// output.
|
||||
//
|
||||
// TODO: find a link for this syntax. It's been copied out of
|
||||
// code, not a specification:
|
||||
//
|
||||
// https://github.com/stripe/veneur/blob/master/sinks/datadog/datadog.go
|
||||
Exporter struct {
|
||||
*statsd.Exporter
|
||||
|
||||
labelEncoder *statsd.LabelEncoder
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
_ export.Exporter = &Exporter{}
|
||||
)
|
||||
|
||||
// NewRawExporter returns a new Dogstatsd-syntax exporter for use in a pipeline.
|
||||
func NewRawExporter(config Config) (*Exporter, error) {
|
||||
exp := &Exporter{
|
||||
labelEncoder: statsd.NewLabelEncoder(),
|
||||
}
|
||||
|
||||
var err error
|
||||
exp.Exporter, err = statsd.NewExporter(config, exp)
|
||||
return exp, err
|
||||
}
|
||||
|
||||
// InstallNewPipeline instantiates a NewExportPipeline and registers it globally.
|
||||
// Typically called as:
|
||||
//
|
||||
// pipeline, err := dogstatsd.InstallNewPipeline(dogstatsd.Config{...})
|
||||
// if err != nil {
|
||||
// ...
|
||||
// }
|
||||
// defer pipeline.Stop()
|
||||
// ... Done
|
||||
func InstallNewPipeline(config Config) (*push.Controller, error) {
|
||||
controller, err := NewExportPipeline(config, time.Minute)
|
||||
if err != nil {
|
||||
return controller, err
|
||||
}
|
||||
global.SetMeterProvider(controller)
|
||||
return controller, err
|
||||
}
|
||||
|
||||
// NewExportPipeline sets up a complete export pipeline with the recommended setup,
|
||||
// chaining a NewRawExporter into the recommended selectors and batchers.
|
||||
func NewExportPipeline(config Config, period time.Duration) (*push.Controller, error) {
|
||||
selector := simple.NewWithExactMeasure()
|
||||
exporter, err := NewRawExporter(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The ungrouped batcher ensures that the export sees the full
|
||||
// set of labels as dogstatsd tags.
|
||||
batcher := ungrouped.New(selector, exporter.labelEncoder, false)
|
||||
|
||||
pusher := push.New(batcher, exporter, period)
|
||||
pusher.Start()
|
||||
|
||||
return pusher, nil
|
||||
}
|
||||
|
||||
// AppendName is part of the stats-internal adapter interface.
|
||||
func (*Exporter) AppendName(rec export.Record, buf *bytes.Buffer) {
|
||||
_, _ = buf.WriteString(rec.Descriptor().Name())
|
||||
}
|
||||
|
||||
// AppendTags is part of the stats-internal adapter interface.
|
||||
func (e *Exporter) AppendTags(rec export.Record, buf *bytes.Buffer) {
|
||||
encoded := rec.Labels().Encoded(e.labelEncoder)
|
||||
_, _ = buf.WriteString(encoded)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dogstatsd_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.opentelemetry.io/otel/api/core"
|
||||
"go.opentelemetry.io/otel/api/key"
|
||||
"go.opentelemetry.io/otel/api/metric"
|
||||
"go.opentelemetry.io/otel/exporters/metric/dogstatsd"
|
||||
"go.opentelemetry.io/otel/exporters/metric/internal/statsd"
|
||||
"go.opentelemetry.io/otel/exporters/metric/test"
|
||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/sum"
|
||||
)
|
||||
|
||||
// TestDogstatsLabels that labels are formatted in the correct style,
|
||||
// whether or not the provided labels were encoded by a statsd label
|
||||
// encoder.
|
||||
func TestDogstatsLabels(t *testing.T) {
|
||||
encoder := statsd.NewLabelEncoder()
|
||||
ctx := context.Background()
|
||||
checkpointSet := test.NewCheckpointSet(encoder)
|
||||
|
||||
desc := metric.NewDescriptor("test.name", metric.CounterKind, core.Int64NumberKind)
|
||||
cagg := sum.New()
|
||||
_ = cagg.Update(ctx, core.NewInt64Number(123), &desc)
|
||||
cagg.Checkpoint(ctx, &desc)
|
||||
|
||||
checkpointSet.Add(&desc, cagg, key.New("A").String("B"))
|
||||
|
||||
var buf bytes.Buffer
|
||||
exp, err := dogstatsd.NewRawExporter(dogstatsd.Config{
|
||||
Writer: &buf,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
err = exp.Export(ctx, checkpointSet)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "test.name:123|c|#A:B\n", buf.String())
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dogstatsd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/api/key"
|
||||
"go.opentelemetry.io/otel/api/metric"
|
||||
"go.opentelemetry.io/otel/exporters/metric/dogstatsd"
|
||||
)
|
||||
|
||||
func ExampleNew() {
|
||||
// Create a "server"
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
var buf [4096]byte
|
||||
n, err := reader.Read(buf[:])
|
||||
if err == io.EOF {
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Fatal("Read err: ", err)
|
||||
} else if n >= len(buf) {
|
||||
log.Fatal("Read small buffer: ", n)
|
||||
} else {
|
||||
fmt.Print(string(buf[0:n]))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Create a meter
|
||||
pusher, err := dogstatsd.NewExportPipeline(dogstatsd.Config{
|
||||
// The Writer field provides test support.
|
||||
Writer: writer,
|
||||
|
||||
// In real code, use the URL field:
|
||||
//
|
||||
// URL: fmt.Sprint("unix://", path),
|
||||
}, time.Minute)
|
||||
if err != nil {
|
||||
log.Fatal("Could not initialize dogstatsd exporter:", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
key := key.New("key")
|
||||
|
||||
// pusher implements the metric.MeterProvider interface:
|
||||
meter := pusher.Meter("example")
|
||||
|
||||
// Create and update a single counter:
|
||||
counter := metric.Must(meter).NewInt64Counter("a.counter", metric.WithKeys(key))
|
||||
labels := meter.Labels(key.String("value"))
|
||||
|
||||
counter.Add(ctx, 100, labels)
|
||||
|
||||
// Flush the exporter, close the pipe, and wait for the reader.
|
||||
pusher.Stop()
|
||||
writer.Close()
|
||||
wg.Wait()
|
||||
|
||||
// Output:
|
||||
// a.counter:100|c|#key:value
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package statsd
|
||||
|
||||
// See https://github.com/b/statsd_spec for the best-available statsd
|
||||
// syntax specification. See also
|
||||
// https://github.com/statsd/statsd/edit/master/docs/metric_types.md
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"go.opentelemetry.io/otel/api/core"
|
||||
"go.opentelemetry.io/otel/api/unit"
|
||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||
)
|
||||
|
||||
type (
|
||||
// Config supports common configuration that applies to statsd exporters.
|
||||
Config struct {
|
||||
// URL describes the destination for exporting statsd data.
|
||||
// e.g., udp://host:port
|
||||
// tcp://host:port
|
||||
// unix:///socket/path
|
||||
URL string
|
||||
|
||||
// Writer is an alternate to providing a URL. When Writer is
|
||||
// non-nil, URL will be ignored and the exporter will write to
|
||||
// the configured Writer interface.
|
||||
Writer io.Writer
|
||||
|
||||
// MaxPacketSize this limits the packet size for packet-oriented transports.
|
||||
MaxPacketSize int
|
||||
|
||||
// TODO support Dial and Write timeouts
|
||||
}
|
||||
|
||||
// Exporter is common type meant to implement concrete statsd
|
||||
// exporters.
|
||||
Exporter struct {
|
||||
adapter Adapter
|
||||
config Config
|
||||
conn net.Conn
|
||||
writer io.Writer
|
||||
buffer bytes.Buffer
|
||||
}
|
||||
|
||||
// Adapter supports statsd syntax variations, primarily plain
|
||||
// statsd vs. dogstatsd.
|
||||
Adapter interface {
|
||||
AppendName(export.Record, *bytes.Buffer)
|
||||
AppendTags(export.Record, *bytes.Buffer)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
formatCounter = "c"
|
||||
formatHistogram = "h"
|
||||
formatGauge = "g"
|
||||
formatTiming = "ms"
|
||||
|
||||
MaxPacketSize = 1 << 16
|
||||
)
|
||||
|
||||
var (
|
||||
_ export.Exporter = &Exporter{}
|
||||
|
||||
ErrInvalidScheme = fmt.Errorf("invalid statsd transport")
|
||||
)
|
||||
|
||||
// NewExporter returns a common implementation for exporters that Export
|
||||
// statsd syntax.
|
||||
func NewExporter(config Config, adapter Adapter) (*Exporter, error) {
|
||||
if config.MaxPacketSize <= 0 {
|
||||
config.MaxPacketSize = MaxPacketSize
|
||||
}
|
||||
var writer io.Writer
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if config.Writer != nil {
|
||||
writer = config.Writer
|
||||
} else {
|
||||
conn, err = dial(config.URL)
|
||||
if conn != nil {
|
||||
writer = conn
|
||||
}
|
||||
}
|
||||
// TODO: If err != nil, we return it _with_ a valid exporter; the
|
||||
// exporter should attempt to re-dial if it's retryable. Add a
|
||||
// Start() and Stop() API.
|
||||
return &Exporter{
|
||||
adapter: adapter,
|
||||
config: config,
|
||||
conn: conn,
|
||||
writer: writer,
|
||||
}, err
|
||||
}
|
||||
|
||||
// dial connects to a statsd service using several common network
|
||||
// types. Presently "udp" and "unix" datagram socket connections are
|
||||
// supported.
|
||||
func dial(endpoint string) (net.Conn, error) {
|
||||
dest, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Support tcp destination, need configurable timeouts first.
|
||||
|
||||
scheme := dest.Scheme
|
||||
switch scheme {
|
||||
case "udp", "udp4", "udp6":
|
||||
udpAddr, err := net.ResolveUDPAddr(scheme, dest.Host)
|
||||
locAddr := &net.UDPAddr{}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := net.DialUDP(scheme, locAddr, udpAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, err
|
||||
case "unix", "unixgram":
|
||||
scheme = "unixgram"
|
||||
locAddr := &net.UnixAddr{}
|
||||
|
||||
sockAddr, err := net.ResolveUnixAddr(scheme, dest.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := net.DialUnix(scheme, locAddr, sockAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
return nil, ErrInvalidScheme
|
||||
}
|
||||
|
||||
// Export is common code for any statsd-based metric.Exporter implementation.
|
||||
func (e *Exporter) Export(_ context.Context, checkpointSet export.CheckpointSet) error {
|
||||
buf := &e.buffer
|
||||
buf.Reset()
|
||||
|
||||
var aggErr error
|
||||
var sendErr error
|
||||
|
||||
aggErr = checkpointSet.ForEach(func(rec export.Record) error {
|
||||
before := buf.Len()
|
||||
|
||||
if err := e.formatMetric(rec, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if buf.Len() < e.config.MaxPacketSize {
|
||||
return nil
|
||||
}
|
||||
if before == 0 {
|
||||
// A single metric >= packet size
|
||||
if err := e.send(buf.Bytes()); err != nil && sendErr == nil {
|
||||
sendErr = err
|
||||
}
|
||||
buf.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send and copy the leftover
|
||||
if err := e.send(buf.Bytes()[:before]); err != nil && sendErr == nil {
|
||||
sendErr = err
|
||||
}
|
||||
|
||||
leftover := buf.Len() - before
|
||||
|
||||
copy(buf.Bytes()[0:leftover], buf.Bytes()[before:])
|
||||
|
||||
buf.Truncate(leftover)
|
||||
return nil
|
||||
})
|
||||
if err := e.send(buf.Bytes()); err != nil && sendErr == nil {
|
||||
sendErr = err
|
||||
}
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
return aggErr
|
||||
}
|
||||
|
||||
// send writes a complete buffer to the writer as a blocking call.
|
||||
func (e *Exporter) send(buf []byte) error {
|
||||
for len(buf) != 0 {
|
||||
n, err := e.writer.Write(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatMetric formats an individual export record. For some records
|
||||
// this will emit a single statistic, for some it will emit more than
|
||||
// one.
|
||||
func (e *Exporter) formatMetric(rec export.Record, buf *bytes.Buffer) error {
|
||||
desc := rec.Descriptor()
|
||||
agg := rec.Aggregator()
|
||||
|
||||
// TODO handle non-Points Distribution/MaxSumCount by
|
||||
// formatting individual quantiles, the sum, and the count as
|
||||
// single statistics. For the dogstatsd variation, assuming
|
||||
// open-source systems like Veneur add support, figure out the
|
||||
// proper encoding for "d"-type distribution data.
|
||||
|
||||
if pts, ok := agg.(aggregator.Points); ok {
|
||||
var format string
|
||||
if desc.Unit() == unit.Milliseconds {
|
||||
format = formatTiming
|
||||
} else {
|
||||
format = formatHistogram
|
||||
}
|
||||
points, err := pts.Points()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pt := range points {
|
||||
e.formatSingleStat(rec, pt, format, buf)
|
||||
}
|
||||
|
||||
} else if sum, ok := agg.(aggregator.Sum); ok {
|
||||
sum, err := sum.Sum()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.formatSingleStat(rec, sum, formatCounter, buf)
|
||||
|
||||
} else if lv, ok := agg.(aggregator.LastValue); ok {
|
||||
lv, _, err := lv.LastValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.formatSingleStat(rec, lv, formatGauge, buf)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatSingleStat encodes a single item of statsd data followed by a
|
||||
// newline.
|
||||
func (e *Exporter) formatSingleStat(rec export.Record, val core.Number, fmtStr string, buf *bytes.Buffer) {
|
||||
e.adapter.AppendName(rec, buf)
|
||||
_, _ = buf.WriteRune(':')
|
||||
writeNumber(buf, val, rec.Descriptor().NumberKind())
|
||||
_, _ = buf.WriteRune('|')
|
||||
_, _ = buf.WriteString(fmtStr)
|
||||
e.adapter.AppendTags(rec, buf)
|
||||
_, _ = buf.WriteRune('\n')
|
||||
}
|
||||
|
||||
func writeNumber(buf *bytes.Buffer, num core.Number, kind core.NumberKind) {
|
||||
var tmp [128]byte
|
||||
var conv []byte
|
||||
switch kind {
|
||||
case core.Int64NumberKind:
|
||||
conv = strconv.AppendInt(tmp[:0], num.AsInt64(), 10)
|
||||
case core.Float64NumberKind:
|
||||
conv = strconv.AppendFloat(tmp[:0], num.AsFloat64(), 'g', -1, 64)
|
||||
case core.Uint64NumberKind:
|
||||
conv = strconv.AppendUint(tmp[:0], num.AsUint64(), 10)
|
||||
|
||||
}
|
||||
_, _ = buf.Write(conv)
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package statsd_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.opentelemetry.io/otel/api/core"
|
||||
"go.opentelemetry.io/otel/api/key"
|
||||
"go.opentelemetry.io/otel/api/metric"
|
||||
"go.opentelemetry.io/otel/api/unit"
|
||||
"go.opentelemetry.io/otel/exporters/metric/internal/statsd"
|
||||
"go.opentelemetry.io/otel/exporters/metric/test"
|
||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||
)
|
||||
|
||||
// withTagsAdapter tests a dogstatsd-style statsd exporter.
|
||||
type withTagsAdapter struct {
|
||||
*statsd.LabelEncoder
|
||||
}
|
||||
|
||||
func (*withTagsAdapter) AppendName(rec export.Record, buf *bytes.Buffer) {
|
||||
_, _ = buf.WriteString(rec.Descriptor().Name())
|
||||
}
|
||||
|
||||
func (ta *withTagsAdapter) AppendTags(rec export.Record, buf *bytes.Buffer) {
|
||||
encoded := rec.Labels().Encoded(ta.LabelEncoder)
|
||||
_, _ = buf.WriteString(encoded)
|
||||
}
|
||||
|
||||
func newWithTagsAdapter() *withTagsAdapter {
|
||||
return &withTagsAdapter{
|
||||
statsd.NewLabelEncoder(),
|
||||
}
|
||||
}
|
||||
|
||||
// noTagsAdapter simulates a plain-statsd exporter that appends tag
|
||||
// values to the metric name.
|
||||
type noTagsAdapter struct {
|
||||
}
|
||||
|
||||
func (*noTagsAdapter) AppendName(rec export.Record, buf *bytes.Buffer) {
|
||||
_, _ = buf.WriteString(rec.Descriptor().Name())
|
||||
|
||||
iter := rec.Labels().Iter()
|
||||
for iter.Next() {
|
||||
tag := iter.Label()
|
||||
_, _ = buf.WriteString(".")
|
||||
_, _ = buf.WriteString(tag.Value.Emit())
|
||||
}
|
||||
}
|
||||
|
||||
func (*noTagsAdapter) AppendTags(rec export.Record, buf *bytes.Buffer) {
|
||||
}
|
||||
|
||||
func newNoTagsAdapter() *noTagsAdapter {
|
||||
return &noTagsAdapter{}
|
||||
}
|
||||
|
||||
type testWriter struct {
|
||||
vec []string
|
||||
}
|
||||
|
||||
func (w *testWriter) Write(b []byte) (int, error) {
|
||||
w.vec = append(w.vec, string(b))
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func TestBasicFormat(t *testing.T) {
|
||||
type adapterOutput struct {
|
||||
adapter statsd.Adapter
|
||||
expected string
|
||||
}
|
||||
|
||||
for _, ao := range []adapterOutput{{
|
||||
adapter: newWithTagsAdapter(),
|
||||
expected: `counter:%s|c|#A:B,C:D
|
||||
observer:%s|g|#A:B,C:D
|
||||
measure:%s|h|#A:B,C:D
|
||||
timer:%s|ms|#A:B,C:D
|
||||
`}, {
|
||||
adapter: newNoTagsAdapter(),
|
||||
expected: `counter.B.D:%s|c
|
||||
observer.B.D:%s|g
|
||||
measure.B.D:%s|h
|
||||
timer.B.D:%s|ms
|
||||
`},
|
||||
} {
|
||||
adapter := ao.adapter
|
||||
expected := ao.expected
|
||||
t.Run(fmt.Sprintf("%T", adapter), func(t *testing.T) {
|
||||
for _, nkind := range []core.NumberKind{
|
||||
core.Float64NumberKind,
|
||||
core.Int64NumberKind,
|
||||
} {
|
||||
t.Run(nkind.String(), func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
writer := &testWriter{}
|
||||
config := statsd.Config{
|
||||
Writer: writer,
|
||||
MaxPacketSize: 1024,
|
||||
}
|
||||
exp, err := statsd.NewExporter(config, adapter)
|
||||
if err != nil {
|
||||
t.Fatal("New error: ", err)
|
||||
}
|
||||
|
||||
checkpointSet := test.NewCheckpointSet(export.NewDefaultLabelEncoder())
|
||||
cdesc := metric.NewDescriptor(
|
||||
"counter", metric.CounterKind, nkind)
|
||||
gdesc := metric.NewDescriptor(
|
||||
"observer", metric.ObserverKind, nkind)
|
||||
mdesc := metric.NewDescriptor(
|
||||
"measure", metric.MeasureKind, nkind)
|
||||
tdesc := metric.NewDescriptor(
|
||||
"timer", metric.MeasureKind, nkind, metric.WithUnit(unit.Milliseconds))
|
||||
|
||||
labels := []core.KeyValue{
|
||||
key.New("A").String("B"),
|
||||
key.New("C").String("D"),
|
||||
}
|
||||
const value = 123.456
|
||||
|
||||
checkpointSet.AddCounter(&cdesc, value, labels...)
|
||||
checkpointSet.AddLastValue(&gdesc, value, labels...)
|
||||
checkpointSet.AddMeasure(&mdesc, value, labels...)
|
||||
checkpointSet.AddMeasure(&tdesc, value, labels...)
|
||||
|
||||
err = exp.Export(ctx, checkpointSet)
|
||||
require.Nil(t, err)
|
||||
|
||||
var vfmt string
|
||||
if nkind == core.Int64NumberKind {
|
||||
fv := value
|
||||
vfmt = strconv.FormatInt(int64(fv), 10)
|
||||
} else {
|
||||
vfmt = strconv.FormatFloat(value, 'g', -1, 64)
|
||||
}
|
||||
|
||||
require.Equal(t, 1, len(writer.vec))
|
||||
require.Equal(t, fmt.Sprintf(expected, vfmt, vfmt, vfmt, vfmt), writer.vec[0])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func makeLabels(offset, nkeys int) []core.KeyValue {
|
||||
r := make([]core.KeyValue, nkeys)
|
||||
for i := range r {
|
||||
r[i] = key.New(fmt.Sprint("k", offset+i)).String(fmt.Sprint("v", offset+i))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type splitTestCase struct {
|
||||
name string
|
||||
setup func(add func(int))
|
||||
check func(expected, got []string, t *testing.T)
|
||||
}
|
||||
|
||||
var splitTestCases = []splitTestCase{
|
||||
// These test use the number of keys to control where packets
|
||||
// are split.
|
||||
{"Simple",
|
||||
func(add func(int)) {
|
||||
add(1)
|
||||
add(1000)
|
||||
add(1)
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.EqualValues(t, expected, got)
|
||||
},
|
||||
},
|
||||
{"LastBig",
|
||||
func(add func(int)) {
|
||||
add(1)
|
||||
add(1)
|
||||
add(1000)
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.Equal(t, 2, len(got))
|
||||
require.EqualValues(t, []string{
|
||||
expected[0] + expected[1],
|
||||
expected[2],
|
||||
}, got)
|
||||
},
|
||||
},
|
||||
{"FirstBig",
|
||||
func(add func(int)) {
|
||||
add(1000)
|
||||
add(1)
|
||||
add(1)
|
||||
add(1000)
|
||||
add(1)
|
||||
add(1)
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.Equal(t, 4, len(got))
|
||||
require.EqualValues(t, []string{
|
||||
expected[0],
|
||||
expected[1] + expected[2],
|
||||
expected[3],
|
||||
expected[4] + expected[5],
|
||||
}, got)
|
||||
},
|
||||
},
|
||||
{"OneBig",
|
||||
func(add func(int)) {
|
||||
add(1000)
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.EqualValues(t, expected, got)
|
||||
},
|
||||
},
|
||||
{"LastSmall",
|
||||
func(add func(int)) {
|
||||
add(1000)
|
||||
add(1)
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.EqualValues(t, expected, got)
|
||||
},
|
||||
},
|
||||
{"Overflow",
|
||||
func(add func(int)) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
add(1)
|
||||
}
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.Less(t, 1, len(got))
|
||||
require.Equal(t, strings.Join(expected, ""), strings.Join(got, ""))
|
||||
},
|
||||
},
|
||||
{"Empty",
|
||||
func(add func(int)) {
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.Equal(t, 0, len(got))
|
||||
},
|
||||
},
|
||||
{"AllBig",
|
||||
func(add func(int)) {
|
||||
add(1000)
|
||||
add(1000)
|
||||
add(1000)
|
||||
},
|
||||
func(expected, got []string, t *testing.T) {
|
||||
require.EqualValues(t, expected, got)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestPacketSplit(t *testing.T) {
|
||||
for _, tcase := range splitTestCases {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
writer := &testWriter{}
|
||||
config := statsd.Config{
|
||||
Writer: writer,
|
||||
MaxPacketSize: 1024,
|
||||
}
|
||||
adapter := newWithTagsAdapter()
|
||||
exp, err := statsd.NewExporter(config, adapter)
|
||||
if err != nil {
|
||||
t.Fatal("New error: ", err)
|
||||
}
|
||||
|
||||
checkpointSet := test.NewCheckpointSet(adapter.LabelEncoder)
|
||||
desc := metric.NewDescriptor("counter", metric.CounterKind, core.Int64NumberKind)
|
||||
|
||||
var expected []string
|
||||
|
||||
offset := 0
|
||||
tcase.setup(func(nkeys int) {
|
||||
labels := makeLabels(offset, nkeys)
|
||||
offset += nkeys
|
||||
iter := export.LabelSlice(labels).Iter()
|
||||
encoded := adapter.LabelEncoder.Encode(iter)
|
||||
expect := fmt.Sprint("counter:100|c", encoded, "\n")
|
||||
expected = append(expected, expect)
|
||||
checkpointSet.AddCounter(&desc, 100, labels...)
|
||||
})
|
||||
|
||||
err = exp.Export(ctx, checkpointSet)
|
||||
require.Nil(t, err)
|
||||
|
||||
tcase.check(expected, writer.vec, t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
|
||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||
)
|
||||
|
||||
// LabelEncoder encodes metric labels in the dogstatsd syntax.
|
||||
//
|
||||
// TODO: find a link for this syntax. It's been copied out of code,
|
||||
// not a specification:
|
||||
//
|
||||
// https://github.com/stripe/veneur/blob/master/sinks/datadog/datadog.go
|
||||
type LabelEncoder struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
var _ export.LabelEncoder = &LabelEncoder{}
|
||||
var leID = export.NewLabelEncoderID()
|
||||
|
||||
// NewLabelEncoder returns a new encoder for dogstatsd-syntax metric
|
||||
// labels.
|
||||
func NewLabelEncoder() *LabelEncoder {
|
||||
return &LabelEncoder{
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &bytes.Buffer{}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Encode emits a string like "|#key1:value1,key2:value2".
|
||||
func (e *LabelEncoder) Encode(iter export.LabelIterator) string {
|
||||
buf := e.pool.Get().(*bytes.Buffer)
|
||||
defer e.pool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
delimiter := "|#"
|
||||
|
||||
for iter.Next() {
|
||||
kv := iter.Label()
|
||||
_, _ = buf.WriteString(delimiter)
|
||||
_, _ = buf.WriteString(string(kv.Key))
|
||||
_, _ = buf.WriteRune(':')
|
||||
_, _ = buf.WriteString(kv.Value.Emit())
|
||||
delimiter = ","
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (*LabelEncoder) ID() int64 {
|
||||
return leID
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package statsd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.opentelemetry.io/otel/api/core"
|
||||
"go.opentelemetry.io/otel/api/key"
|
||||
"go.opentelemetry.io/otel/exporters/metric/internal/statsd"
|
||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||
)
|
||||
|
||||
var testLabels = []core.KeyValue{
|
||||
key.New("A").String("B"),
|
||||
key.New("C").String("D"),
|
||||
key.New("E").Float64(1.5),
|
||||
}
|
||||
|
||||
func TestLabelSyntax(t *testing.T) {
|
||||
encoder := statsd.NewLabelEncoder()
|
||||
|
||||
require.Equal(t, `|#A:B,C:D,E:1.5`, encoder.Encode(export.LabelSlice(testLabels).Iter()))
|
||||
|
||||
kvs := []core.KeyValue{
|
||||
key.New("A").String("B"),
|
||||
}
|
||||
require.Equal(t, `|#A:B`, encoder.Encode(export.LabelSlice(kvs).Iter()))
|
||||
|
||||
require.Equal(t, "", encoder.Encode(export.LabelSlice(nil).Iter()))
|
||||
}
|
||||
Reference in New Issue
Block a user