From 452256cbf419c51b3cec0bdf64c0cff64980e9f4 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 22 Jul 2020 12:34:44 -0700 Subject: [PATCH] Unify trace and metric stdout exporters (#956) * Consolidate stdout exporter * Move config to own file and match project standard * Abstract Exporter into unified struct * Rename trace part of the exporter * Update import paths and configuration * Update tests * Update InstallNewPipeline to not return traceProvider It is a registered global, access it that way. * Update example_test * Update docs * Update example to be for whole package * Update metric output Closer match the span output. * Clean up span output Print as a batch and cleanup marshaling. * Correct spelling error in doc * Add Exporters README * Update Changelog * Propagate changes to rest of project * Lint fixes * Fix example test in metric SDK * Add disable config options for trace and metric Co-authored-by: Liz Fong-Jones --- CHANGELOG.md | 1 + README.md | 16 +- api/global/internal/meter_test.go | 22 +- example/basic/main.go | 44 +-- example/grpc/config/config.go | 4 +- example/http/client/client.go | 4 +- example/http/server/server.go | 4 +- example/namedtracer/main.go | 4 +- exporters/README.md | 18 ++ exporters/metric/stdout/example_test.go | 60 ---- exporters/metric/stdout/stdout.go | 280 ------------------ exporters/stdout/config.go | 175 +++++++++++ exporters/{trace => }/stdout/doc.go | 5 +- exporters/stdout/example_test.go | 91 ++++++ exporters/stdout/exporter.go | 94 ++++++ exporters/stdout/metric.go | 186 ++++++++++++ .../stdout_test.go => stdout/metric_test.go} | 124 ++++---- exporters/stdout/trace.go | 58 ++++ .../stdout_test.go => stdout/trace_test.go} | 9 +- exporters/trace/stdout/stdout.go | 68 ----- .../othttp/handler_example_test.go | 30 +- sdk/metric/example_test.go | 42 +-- 22 files changed, 746 insertions(+), 593 deletions(-) create mode 100644 exporters/README.md delete mode 100644 exporters/metric/stdout/example_test.go delete mode 100644 exporters/metric/stdout/stdout.go create mode 100644 exporters/stdout/config.go rename exporters/{trace => }/stdout/doc.go (73%) create mode 100644 exporters/stdout/example_test.go create mode 100644 exporters/stdout/exporter.go create mode 100644 exporters/stdout/metric.go rename exporters/{metric/stdout/stdout_test.go => stdout/metric_test.go} (77%) create mode 100644 exporters/stdout/trace.go rename exporters/{trace/stdout/stdout_test.go => stdout/trace_test.go} (95%) delete mode 100644 exporters/trace/stdout/stdout.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5199d48b670..cfa84e16d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Jaeger exporter helpers: added InstallNewPipeline and removed RegisterGlobal option instead. (#944) - Zipkin exporter helpers: pipeline methods introduced, new exporter method adjusted. (#944) +- The trace (`go.opentelemetry.io/otel/exporters/trace/stdout`) and metric (`go.opentelemetry.io/otel/exporters/metric/stdout`) `stdout` exporters are now merged into a single exporter at `go.opentelemetry.io/otel/exporters/stdout`. (#956) ## [0.9.0] - 2020-07-20 diff --git a/README.md b/README.md index 431fce88a46..3604be53db9 100644 --- a/README.md +++ b/README.md @@ -38,25 +38,17 @@ import ( "log" "go.opentelemetry.io/otel/api/global" - "go.opentelemetry.io/otel/exporters/trace/stdout" + "go.opentelemetry.io/otel/exporters/stdout" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) -func initTracer() { - exporter, err := stdout.NewExporter(stdout.Options{PrettyPrint: true}) - if err != nil { - log.Fatal(err) - } - tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), - sdktrace.WithSyncer(exporter)) +func main() { + pusher, err := stdout.InstallNewPipeline(nil, nil) if err != nil { log.Fatal(err) } - global.SetTraceProvider(tp) -} + defer pusher.Stop() -func main() { - initTracer() tracer := global.Tracer("ex.com/basic") tracer.WithSpan(context.Background(), "foo", diff --git a/api/global/internal/meter_test.go b/api/global/internal/meter_test.go index ca47b464972..5413ac25fbe 100644 --- a/api/global/internal/meter_test.go +++ b/api/global/internal/meter_test.go @@ -30,7 +30,7 @@ import ( "go.opentelemetry.io/otel/api/global/internal" "go.opentelemetry.io/otel/api/kv" "go.opentelemetry.io/otel/api/metric" - "go.opentelemetry.io/otel/exporters/metric/stdout" + "go.opentelemetry.io/otel/exporters/stdout" metrictest "go.opentelemetry.io/otel/internal/metric" ) @@ -243,10 +243,10 @@ func TestDefaultSDK(t *testing.T) { counter.Add(ctx, 1, labels1...) in, out := io.Pipe() - pusher, err := stdout.InstallNewPipeline(stdout.Config{ - Writer: out, - DoNotPrintTime: true, - }) + pusher, err := stdout.InstallNewPipeline([]stdout.Option{ + stdout.WithWriter(out), + stdout.WithoutTimestamps(), + }, nil) if err != nil { panic(err) } @@ -262,7 +262,7 @@ func TestDefaultSDK(t *testing.T) { pusher.Stop() out.Close() - require.Equal(t, `{"updates":[{"name":"test.builtin{instrumentation.name=builtin,A=B}","sum":1}]} + require.Equal(t, `[{"Name":"test.builtin{instrumentation.name=builtin,A=B}","Sum":1}] `, <-ch) } @@ -408,10 +408,10 @@ func TestRecordBatchRealSDK(t *testing.T) { var buf bytes.Buffer - pusher, err := stdout.InstallNewPipeline(stdout.Config{ - Writer: &buf, - DoNotPrintTime: true, - }) + pusher, err := stdout.InstallNewPipeline([]stdout.Option{ + stdout.WithWriter(&buf), + stdout.WithoutTimestamps(), + }, nil) if err != nil { t.Fatal(err) } @@ -420,6 +420,6 @@ func TestRecordBatchRealSDK(t *testing.T) { meter.RecordBatch(context.Background(), nil, counter.Measurement(1)) pusher.Stop() - require.Equal(t, `{"updates":[{"name":"test.counter{instrumentation.name=builtin}","sum":1}]} + require.Equal(t, `[{"Name":"test.counter{instrumentation.name=builtin}","Sum":1}] `, buf.String()) } diff --git a/example/basic/main.go b/example/basic/main.go index 91135004978..90dc59466cd 100644 --- a/example/basic/main.go +++ b/example/basic/main.go @@ -23,11 +23,7 @@ import ( "go.opentelemetry.io/otel/api/kv" "go.opentelemetry.io/otel/api/metric" "go.opentelemetry.io/otel/api/trace" - metricstdout "go.opentelemetry.io/otel/exporters/metric/stdout" - tracestdout "go.opentelemetry.io/otel/exporters/trace/stdout" - "go.opentelemetry.io/otel/sdk/metric/controller/push" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/exporters/stdout" ) var ( @@ -37,37 +33,15 @@ var ( anotherKey = kv.Key("ex.com/another") ) -// initTracer creates and registers trace provider instance. -func initTracer() { - var err error - exp, err := tracestdout.NewExporter(tracestdout.Options{PrettyPrint: false}) - if err != nil { - log.Panicf("failed to initialize trace stdout exporter %v", err) - return - } - tp, err := sdktrace.NewProvider(sdktrace.WithSyncer(exp), - sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), - sdktrace.WithResource(resource.New(kv.String("rk1", "rv11"), kv.Int64("rk2", 5)))) - if err != nil { - log.Panicf("failed to initialize trace provider %v", err) - } - global.SetTraceProvider(tp) -} - -func initMeter() *push.Controller { - pusher, err := metricstdout.InstallNewPipeline(metricstdout.Config{ - Quantiles: []float64{0.5, 0.9, 0.99}, - PrettyPrint: false, - }) +func main() { + pusher, err := stdout.InstallNewPipeline([]stdout.Option{ + stdout.WithQuantiles([]float64{0.5, 0.9, 0.99}), + stdout.WithPrettyPrint(), + }, nil) if err != nil { - log.Panicf("failed to initialize metric stdout exporter %v", err) + log.Fatalf("failed to initialize stdout export pipeline: %v", err) } - return pusher -} - -func main() { - defer initMeter().Stop() - initTracer() + defer pusher.Stop() tracer := global.Tracer("ex.com/basic") meter := global.Meter("ex.com/basic") @@ -93,7 +67,7 @@ func main() { valuerecorder := valuerecorderTwo.Bind(commonLabels...) defer valuerecorder.Unbind() - err := tracer.WithSpan(ctx, "operation", func(ctx context.Context) error { + err = tracer.WithSpan(ctx, "operation", func(ctx context.Context) error { trace.SpanFromContext(ctx).AddEvent(ctx, "Nice operation!", kv.Key("bogons").Int(100)) diff --git a/example/grpc/config/config.go b/example/grpc/config/config.go index f15b5b6db07..9c1034e7ebf 100644 --- a/example/grpc/config/config.go +++ b/example/grpc/config/config.go @@ -18,13 +18,13 @@ import ( "log" "go.opentelemetry.io/otel/api/global" - "go.opentelemetry.io/otel/exporters/trace/stdout" + "go.opentelemetry.io/otel/exporters/stdout" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) // Init configures an OpenTelemetry exporter and trace provider func Init() { - exporter, err := stdout.NewExporter(stdout.Options{PrettyPrint: true}) + exporter, err := stdout.NewExporter(stdout.WithPrettyPrint()) if err != nil { log.Fatal(err) } diff --git a/example/http/client/client.go b/example/http/client/client.go index d63ea9579ac..2a4529bb38a 100644 --- a/example/http/client/client.go +++ b/example/http/client/client.go @@ -30,7 +30,7 @@ import ( "go.opentelemetry.io/otel/api/correlation" "go.opentelemetry.io/otel/api/global" - "go.opentelemetry.io/otel/exporters/trace/stdout" + "go.opentelemetry.io/otel/exporters/stdout" "go.opentelemetry.io/otel/instrumentation/httptrace" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) @@ -38,7 +38,7 @@ import ( func initTracer() { // Create stdout exporter to be able to retrieve // the collected spans. - exporter, err := stdout.NewExporter(stdout.Options{PrettyPrint: true}) + exporter, err := stdout.NewExporter(stdout.WithPrettyPrint()) if err != nil { log.Fatal(err) } diff --git a/example/http/server/server.go b/example/http/server/server.go index b6944d5f3bb..d3543818881 100644 --- a/example/http/server/server.go +++ b/example/http/server/server.go @@ -23,7 +23,7 @@ import ( "go.opentelemetry.io/otel/api/global" "go.opentelemetry.io/otel/api/standard" "go.opentelemetry.io/otel/api/trace" - "go.opentelemetry.io/otel/exporters/trace/stdout" + "go.opentelemetry.io/otel/exporters/stdout" "go.opentelemetry.io/otel/instrumentation/httptrace" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -32,7 +32,7 @@ import ( func initTracer() { // Create stdout exporter to be able to retrieve // the collected spans. - exporter, err := stdout.NewExporter(stdout.Options{PrettyPrint: true}) + exporter, err := stdout.NewExporter(stdout.WithPrettyPrint()) if err != nil { log.Fatal(err) } diff --git a/example/namedtracer/main.go b/example/namedtracer/main.go index f0aae45ad75..a98abefc04b 100644 --- a/example/namedtracer/main.go +++ b/example/namedtracer/main.go @@ -24,7 +24,7 @@ import ( "go.opentelemetry.io/otel/api/global" "go.opentelemetry.io/otel/api/trace" "go.opentelemetry.io/otel/example/namedtracer/foo" - "go.opentelemetry.io/otel/exporters/trace/stdout" + "go.opentelemetry.io/otel/exporters/stdout" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) @@ -39,7 +39,7 @@ var tp *sdktrace.Provider // initTracer creates and registers trace provider instance. func initTracer() { var err error - exp, err := stdout.NewExporter(stdout.Options{}) + exp, err := stdout.NewExporter(stdout.WithPrettyPrint()) if err != nil { log.Panicf("failed to initialize stdout exporter %v\n", err) return diff --git a/exporters/README.md b/exporters/README.md new file mode 100644 index 00000000000..13d52bbfe8a --- /dev/null +++ b/exporters/README.md @@ -0,0 +1,18 @@ +# Exporters + +Included in this directory are exporters that export both metric and trace telemetry. + +- [stdout](./stdout): Writes telemetry to a specified local output as structured JSON. +- [otlp](./otlp): Sends telemetry to an OpenTelemetry collector as OTLP. + +Additionally, there are [metric](./metric) and [trace](./trace) only exporters. + +## Metric Telemetry Only + +- [prometheus](./metric/prometheus): Exposes metric telemetry as Prometheus metrics. +- [test](./metric/test): A development tool when testing the telemetry pipeline. + +## Trace Telemetry Only + +- [jaeger](./trace/jaeger): Sends properly transformed trace telemetry to a Jaeger endpoint. +- [zipkin](./trace/zipkin): Sends properly transformed trace telemetry to a Zipkin endpoint. diff --git a/exporters/metric/stdout/example_test.go b/exporters/metric/stdout/example_test.go deleted file mode 100644 index 60861cdc27d..00000000000 --- a/exporters/metric/stdout/example_test.go +++ /dev/null @@ -1,60 +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 stdout_test - -import ( - "context" - "log" - - "go.opentelemetry.io/otel/api/kv" - "go.opentelemetry.io/otel/api/metric" - "go.opentelemetry.io/otel/exporters/metric/stdout" -) - -func ExampleNewExportPipeline() { - // Create a meter - pusher, err := stdout.NewExportPipeline(stdout.Config{ - PrettyPrint: true, - DoNotPrintTime: true, - }) - if err != nil { - log.Fatal("Could not initialize stdout exporter:", err) - } - defer pusher.Stop() - - ctx := context.Background() - - key := kv.Key("key") - meter := pusher.Provider().Meter( - "github.com/instrumentron", - metric.WithInstrumentationVersion("v0.1.0"), - ) - - // Create and update a single counter: - counter := metric.Must(meter).NewInt64Counter("a.counter") - labels := []kv.KeyValue{key.String("value")} - - counter.Add(ctx, 100, labels...) - - // Output: - // { - // "updates": [ - // { - // "name": "a.counter{instrumentation.name=github.com/instrumentron,instrumentation.version=v0.1.0,key=value}", - // "sum": 100 - // } - // ] - // } -} diff --git a/exporters/metric/stdout/stdout.go b/exporters/metric/stdout/stdout.go deleted file mode 100644 index 4ffadb021c2..00000000000 --- a/exporters/metric/stdout/stdout.go +++ /dev/null @@ -1,280 +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 stdout // import "go.opentelemetry.io/otel/exporters/metric/stdout" - -import ( - "context" - "encoding/json" - "fmt" - "io" - "os" - "strings" - "time" - - "go.opentelemetry.io/otel/api/global" - "go.opentelemetry.io/otel/api/kv" - "go.opentelemetry.io/otel/api/label" - "go.opentelemetry.io/otel/api/metric" - export "go.opentelemetry.io/otel/sdk/export/metric" - "go.opentelemetry.io/otel/sdk/export/metric/aggregation" - "go.opentelemetry.io/otel/sdk/metric/controller/push" - "go.opentelemetry.io/otel/sdk/metric/selector/simple" -) - -type Exporter struct { - config Config -} - -var _ export.Exporter = &Exporter{} - -// Config is the configuration to be used when initializing a stdout export. -type Config struct { - // Writer is the destination. If not set, os.Stdout is used. - Writer io.Writer - - // PrettyPrint will pretty the json representation of the span, - // making it print "pretty". Default is false. - PrettyPrint bool - - // DoNotPrintTime suppresses timestamp printing. This is - // useful to create deterministic test conditions. - DoNotPrintTime bool - - // Quantiles are the desired aggregation quantiles for distribution - // summaries, used when the configured aggregator supports - // quantiles. - // - // Note: this exporter is meant as a demonstration; a real - // exporter may wish to configure quantiles on a per-metric - // basis. - Quantiles []float64 - - // LabelEncoder encodes the labels - LabelEncoder label.Encoder -} - -type expoBatch struct { - Timestamp *time.Time `json:"time,omitempty"` - Updates []expoLine `json:"updates"` -} - -type expoLine struct { - Name string `json:"name"` - Min interface{} `json:"min,omitempty"` - Max interface{} `json:"max,omitempty"` - Sum interface{} `json:"sum,omitempty"` - Count interface{} `json:"count,omitempty"` - LastValue interface{} `json:"last,omitempty"` - - Quantiles interface{} `json:"quantiles,omitempty"` - - // Note: this is a pointer because omitempty doesn't work when time.IsZero() - Timestamp *time.Time `json:"time,omitempty"` -} - -type expoQuantile struct { - Q interface{} `json:"q"` - V interface{} `json:"v"` -} - -// NewRawExporter creates a stdout Exporter for use in a pipeline. -func NewRawExporter(config Config) (*Exporter, error) { - if config.Writer == nil { - config.Writer = os.Stdout - } - if config.Quantiles == nil { - config.Quantiles = []float64{0.5, 0.9, 0.99} - } else { - for _, q := range config.Quantiles { - if q < 0 || q > 1 { - return nil, aggregation.ErrInvalidQuantile - } - } - } - if config.LabelEncoder == nil { - config.LabelEncoder = label.DefaultEncoder() - } - return &Exporter{ - config: config, - }, nil -} - -// InstallNewPipeline instantiates a NewExportPipeline and registers it globally. -// Typically called as: -// -// pipeline, err := stdout.InstallNewPipeline(stdout.Config{...}) -// if err != nil { -// ... -// } -// defer pipeline.Stop() -// ... Done -func InstallNewPipeline(config Config, options ...push.Option) (*push.Controller, error) { - controller, err := NewExportPipeline(config, options...) - if err != nil { - return controller, err - } - global.SetMeterProvider(controller.Provider()) - return controller, err -} - -// NewExportPipeline sets up a complete export pipeline with the -// recommended setup, chaining a NewRawExporter into the recommended -// selectors and processors. -func NewExportPipeline(config Config, options ...push.Option) (*push.Controller, error) { - exporter, err := NewRawExporter(config) - if err != nil { - return nil, err - } - pusher := push.New( - simple.NewWithExactDistribution(), - exporter, - options..., - ) - pusher.Start() - - return pusher, nil -} - -func (e *Exporter) ExportKindFor(*metric.Descriptor, aggregation.Kind) export.ExportKind { - return export.PassThroughExporter -} - -func (e *Exporter) Export(_ context.Context, checkpointSet export.CheckpointSet) error { - var aggError error - var batch expoBatch - if !e.config.DoNotPrintTime { - ts := time.Now() - batch.Timestamp = &ts - } - aggError = checkpointSet.ForEach(e, func(record export.Record) error { - desc := record.Descriptor() - agg := record.Aggregation() - kind := desc.NumberKind() - encodedResource := record.Resource().Encoded(e.config.LabelEncoder) - - var instLabels []kv.KeyValue - if name := desc.InstrumentationName(); name != "" { - instLabels = append(instLabels, kv.String("instrumentation.name", name)) - if version := desc.InstrumentationVersion(); version != "" { - instLabels = append(instLabels, kv.String("instrumentation.version", version)) - } - } - instSet := label.NewSet(instLabels...) - encodedInstLabels := instSet.Encoded(e.config.LabelEncoder) - - var expose expoLine - - if sum, ok := agg.(aggregation.Sum); ok { - value, err := sum.Sum() - if err != nil { - return err - } - expose.Sum = value.AsInterface(kind) - } - - if mmsc, ok := agg.(aggregation.MinMaxSumCount); ok { - count, err := mmsc.Count() - if err != nil { - return err - } - expose.Count = count - - max, err := mmsc.Max() - if err != nil { - return err - } - expose.Max = max.AsInterface(kind) - - min, err := mmsc.Min() - if err != nil { - return err - } - expose.Min = min.AsInterface(kind) - - if dist, ok := agg.(aggregation.Distribution); ok && len(e.config.Quantiles) != 0 { - summary := make([]expoQuantile, len(e.config.Quantiles)) - expose.Quantiles = summary - - for i, q := range e.config.Quantiles { - var vstr interface{} - value, err := dist.Quantile(q) - if err != nil { - return err - } - vstr = value.AsInterface(kind) - summary[i] = expoQuantile{ - Q: q, - V: vstr, - } - } - } - } else if lv, ok := agg.(aggregation.LastValue); ok { - value, timestamp, err := lv.LastValue() - if err != nil { - return err - } - expose.LastValue = value.AsInterface(kind) - - if !e.config.DoNotPrintTime { - expose.Timestamp = ×tamp - } - } - - var encodedLabels string - iter := record.Labels().Iter() - if iter.Len() > 0 { - encodedLabels = record.Labels().Encoded(e.config.LabelEncoder) - } - - var sb strings.Builder - - sb.WriteString(desc.Name()) - - if len(encodedLabels) > 0 || len(encodedResource) > 0 || len(encodedInstLabels) > 0 { - sb.WriteRune('{') - sb.WriteString(encodedResource) - if len(encodedInstLabels) > 0 && len(encodedResource) > 0 { - sb.WriteRune(',') - } - sb.WriteString(encodedInstLabels) - if len(encodedLabels) > 0 && (len(encodedInstLabels) > 0 || len(encodedResource) > 0) { - sb.WriteRune(',') - } - sb.WriteString(encodedLabels) - sb.WriteRune('}') - } - - expose.Name = sb.String() - - batch.Updates = append(batch.Updates, expose) - return nil - }) - - var data []byte - var err error - if e.config.PrettyPrint { - data, err = json.MarshalIndent(batch, "", "\t") - } else { - data, err = json.Marshal(batch) - } - - if err == nil { - fmt.Fprintln(e.config.Writer, string(data)) - } else { - return err - } - - return aggError -} diff --git a/exporters/stdout/config.go b/exporters/stdout/config.go new file mode 100644 index 00000000000..2255b560958 --- /dev/null +++ b/exporters/stdout/config.go @@ -0,0 +1,175 @@ +// 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 stdout + +import ( + "io" + "os" + + "go.opentelemetry.io/otel/api/label" + "go.opentelemetry.io/otel/sdk/export/metric/aggregation" +) + +var ( + defaultWriter = os.Stdout + defaultPrettyPrint = false + defaultTimestamps = true + defaultQuantiles = []float64{0.5, 0.9, 0.99} + defaultLabelEncoder = label.DefaultEncoder() + defaultDisableTraceExport = false + defaultDisableMetricExport = false +) + +// Config contains options for the STDOUT exporter. +type Config struct { + // Writer is the destination. If not set, os.Stdout is used. + Writer io.Writer + + // PrettyPrint will encode the output into readable JSON. Default is + // false. + PrettyPrint bool + + // Timestamps specifies if timestamps should be pritted. Default is + // true. + Timestamps bool + + // Quantiles are the desired aggregation quantiles for distribution + // summaries, used when the configured aggregator supports + // quantiles. + // + // Note: this exporter is meant as a demonstration; a real + // exporter may wish to configure quantiles on a per-metric + // basis. + Quantiles []float64 + + // LabelEncoder encodes the labels. + LabelEncoder label.Encoder + + // DisableTraceExport prevents any export of trace telemetry. + DisableTraceExport bool + + // DisableMetricExport prevents any export of metric telemetry. + DisableMetricExport bool +} + +// Configure creates a validated Config configured with options. +func Configure(options ...Option) (Config, error) { + config := Config{ + Writer: defaultWriter, + PrettyPrint: defaultPrettyPrint, + Timestamps: defaultTimestamps, + Quantiles: defaultQuantiles, + LabelEncoder: defaultLabelEncoder, + DisableTraceExport: defaultDisableTraceExport, + DisableMetricExport: defaultDisableMetricExport, + } + for _, opt := range options { + opt.Apply(&config) + + } + for _, q := range config.Quantiles { + if q < 0 || q > 1 { + return config, aggregation.ErrInvalidQuantile + } + } + return config, nil +} + +// Option sets the value of an option for a Config. +type Option interface { + // Apply option value to Config. + Apply(*Config) +} + +// WithWriter sets the export stream destination. +func WithWriter(w io.Writer) Option { + return writerOption{w} +} + +type writerOption struct { + W io.Writer +} + +func (o writerOption) Apply(config *Config) { + config.Writer = o.W +} + +// WithPrettyPrint sets the export stream format to use JSON. +func WithPrettyPrint() Option { + return prettyPrintOption(true) +} + +type prettyPrintOption bool + +func (o prettyPrintOption) Apply(config *Config) { + config.PrettyPrint = bool(o) +} + +// WithoutTimestamps sets the export stream to not include timestamps. +func WithoutTimestamps() Option { + return timestampsOption(false) +} + +type timestampsOption bool + +func (o timestampsOption) Apply(config *Config) { + config.Timestamps = bool(o) +} + +// WithQuantiles sets the quantile values to export. +func WithQuantiles(quantiles []float64) Option { + return quantilesOption(quantiles) +} + +type quantilesOption []float64 + +func (o quantilesOption) Apply(config *Config) { + config.Quantiles = []float64(o) +} + +// WithLabelEncoder sets the label encoder used in export. +func WithLabelEncoder(enc label.Encoder) Option { + return labelEncoderOption{enc} +} + +type labelEncoderOption struct { + LabelEncoder label.Encoder +} + +func (o labelEncoderOption) Apply(config *Config) { + config.LabelEncoder = o.LabelEncoder +} + +// WithoutTraceExport disables all trace exporting. +func WithoutTraceExport() Option { + return disableTraceExportOption(true) +} + +type disableTraceExportOption bool + +func (o disableTraceExportOption) Apply(config *Config) { + config.DisableTraceExport = bool(o) +} + +// WithoutMetricExport disables all metric exporting. +func WithoutMetricExport() Option { + return disableMetricExportOption(true) +} + +type disableMetricExportOption bool + +func (o disableMetricExportOption) Apply(config *Config) { + config.DisableMetricExport = bool(o) +} diff --git a/exporters/trace/stdout/doc.go b/exporters/stdout/doc.go similarity index 73% rename from exporters/trace/stdout/doc.go rename to exporters/stdout/doc.go index d099942d88b..9f577c2e85b 100644 --- a/exporters/trace/stdout/doc.go +++ b/exporters/stdout/doc.go @@ -12,5 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package stdout contains an OpenTelemetry tracing exporter for writing to stdout. -package stdout // import "go.opentelemetry.io/otel/exporters/trace/stdout" +// Package stdout contains an OpenTelemetry exporter for both tracing and +// metric telemetry to be written to an output destination as JSON. +package stdout // import "go.opentelemetry.io/otel/exporters/stdout" diff --git a/exporters/stdout/example_test.go b/exporters/stdout/example_test.go new file mode 100644 index 00000000000..9ad8df6fab8 --- /dev/null +++ b/exporters/stdout/example_test.go @@ -0,0 +1,91 @@ +// 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 stdout_test + +import ( + "context" + "log" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/metric" + "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/exporters/stdout" +) + +const ( + instrumentationName = "github.com/instrumentron" + instrumentationVersion = "v0.1.0" +) + +var ( + tracer = global.TraceProvider().Tracer( + instrumentationName, + trace.WithInstrumentationVersion(instrumentationVersion), + ) + + meter = global.MeterProvider().Meter( + instrumentationName, + metric.WithInstrumentationVersion(instrumentationVersion), + ) + + loopCounter = metric.Must(meter).NewInt64Counter("function.loops") + paramValue = metric.Must(meter).NewFloat64ValueRecorder("function.param") + + nameKey = kv.Key("function.name") +) + +func myFunction(ctx context.Context, values ...float64) error { + nameKV := nameKey.String("myFunction") + boundCount := loopCounter.Bind(nameKV) + boundValue := paramValue.Bind(nameKV) + for _, value := range values { + boundCount.Add(ctx, 1) + boundValue.Record(ctx, value) + } + return nil +} + +func Example() { + exportOpts := []stdout.Option{ + stdout.WithQuantiles([]float64{0.5}), + stdout.WithPrettyPrint(), + } + // Registers both a trace and meter Provider globally. + pusher, err := stdout.InstallNewPipeline(exportOpts, nil) + if err != nil { + log.Fatal("Could not initialize stdout exporter:", err) + } + defer pusher.Stop() + + err = tracer.WithSpan( + context.Background(), + "myFunction/call", + func(ctx context.Context) error { + err := tracer.WithSpan( + ctx, + "internal/call", + func(ctx context.Context) error { return myFunction(ctx, 200, 100, 5000, 600) }, + ) + if err != nil { + return err + } + return myFunction(ctx, 100, 200, 500, 800) + }, + ) + if err != nil { + log.Fatal("Failed to call myFunction", err) + } +} diff --git a/exporters/stdout/exporter.go b/exporters/stdout/exporter.go new file mode 100644 index 00000000000..f8555181370 --- /dev/null +++ b/exporters/stdout/exporter.go @@ -0,0 +1,94 @@ +// 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 stdout + +import ( + "go.opentelemetry.io/otel/api/global" + apitrace "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/sdk/export/metric" + "go.opentelemetry.io/otel/sdk/export/trace" + "go.opentelemetry.io/otel/sdk/metric/controller/push" + "go.opentelemetry.io/otel/sdk/metric/selector/simple" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +type Exporter struct { + traceExporter + metricExporter +} + +var ( + _ metric.Exporter = &Exporter{} + _ trace.SpanSyncer = &Exporter{} + _ trace.SpanBatcher = &Exporter{} +) + +// NewExporter creates an Exporter with the passed options. +func NewExporter(options ...Option) (*Exporter, error) { + config, err := Configure(options...) + if err != nil { + return nil, err + } + return &Exporter{ + traceExporter: traceExporter{config}, + metricExporter: metricExporter{config}, + }, nil +} + +// NewExportPipeline creates a complete export pipeline with the default +// selectors, processors, and trace registration. It is the responsibility +// of the caller to stop the returned push Controller. +func NewExportPipeline(exportOpts []Option, pushOpts []push.Option) (apitrace.Provider, *push.Controller, error) { + exporter, err := NewExporter(exportOpts...) + if err != nil { + return nil, nil, err + } + + tp, err := sdktrace.NewProvider(sdktrace.WithSyncer(exporter)) + if err != nil { + return nil, nil, err + } + + pusher := push.New( + simple.NewWithExactDistribution(), + exporter, + pushOpts..., + ) + pusher.Start() + + return tp, pusher, nil +} + +// InstallNewPipeline creates a complete export pipelines with defaults and +// registers it globally. It is the responsibility of the caller to stop the +// returned push Controller. +// +// Typically this is called as: +// +// pipeline, err := stdout.InstallNewPipeline(stdout.Config{...}) +// if err != nil { +// ... +// } +// defer pipeline.Stop() +// ... Done +func InstallNewPipeline(exportOpts []Option, pushOpts []push.Option) (*push.Controller, error) { + traceProvider, controller, err := NewExportPipeline(exportOpts, pushOpts) + if err != nil { + return controller, err + } + global.SetTraceProvider(traceProvider) + global.SetMeterProvider(controller.Provider()) + return controller, err +} diff --git a/exporters/stdout/metric.go b/exporters/stdout/metric.go new file mode 100644 index 00000000000..a809809a0ae --- /dev/null +++ b/exporters/stdout/metric.go @@ -0,0 +1,186 @@ +// 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 stdout + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/label" + apimetric "go.opentelemetry.io/otel/api/metric" + "go.opentelemetry.io/otel/sdk/export/metric" + "go.opentelemetry.io/otel/sdk/export/metric/aggregation" +) + +type metricExporter struct { + config Config +} + +var _ metric.Exporter = &metricExporter{} + +type line struct { + Name string `json:"Name"` + Min interface{} `json:"Min,omitempty"` + Max interface{} `json:"Max,omitempty"` + Sum interface{} `json:"Sum,omitempty"` + Count interface{} `json:"Count,omitempty"` + LastValue interface{} `json:"Last,omitempty"` + + Quantiles []quantile `json:"Quantiles,omitempty"` + + // Note: this is a pointer because omitempty doesn't work when time.IsZero() + Timestamp *time.Time `json:"Timestamp,omitempty"` +} + +type quantile struct { + Quantile interface{} `json:"Quantile"` + Value interface{} `json:"Value"` +} + +func (e *metricExporter) ExportKindFor(*apimetric.Descriptor, aggregation.Kind) metric.ExportKind { + return metric.PassThroughExporter +} + +func (e *metricExporter) Export(_ context.Context, checkpointSet metric.CheckpointSet) error { + if e.config.DisableMetricExport { + return nil + } + var aggError error + var batch []line + aggError = checkpointSet.ForEach(e, func(record metric.Record) error { + desc := record.Descriptor() + agg := record.Aggregation() + kind := desc.NumberKind() + encodedResource := record.Resource().Encoded(e.config.LabelEncoder) + + var instLabels []kv.KeyValue + if name := desc.InstrumentationName(); name != "" { + instLabels = append(instLabels, kv.String("instrumentation.name", name)) + if version := desc.InstrumentationVersion(); version != "" { + instLabels = append(instLabels, kv.String("instrumentation.version", version)) + } + } + instSet := label.NewSet(instLabels...) + encodedInstLabels := instSet.Encoded(e.config.LabelEncoder) + + var expose line + + if sum, ok := agg.(aggregation.Sum); ok { + value, err := sum.Sum() + if err != nil { + return err + } + expose.Sum = value.AsInterface(kind) + } + + if mmsc, ok := agg.(aggregation.MinMaxSumCount); ok { + count, err := mmsc.Count() + if err != nil { + return err + } + expose.Count = count + + max, err := mmsc.Max() + if err != nil { + return err + } + expose.Max = max.AsInterface(kind) + + min, err := mmsc.Min() + if err != nil { + return err + } + expose.Min = min.AsInterface(kind) + + if dist, ok := agg.(aggregation.Distribution); ok && len(e.config.Quantiles) != 0 { + summary := make([]quantile, len(e.config.Quantiles)) + expose.Quantiles = summary + + for i, q := range e.config.Quantiles { + value, err := dist.Quantile(q) + if err != nil { + return err + } + summary[i] = quantile{ + Quantile: q, + Value: value.AsInterface(kind), + } + } + } + } else if lv, ok := agg.(aggregation.LastValue); ok { + value, timestamp, err := lv.LastValue() + if err != nil { + return err + } + expose.LastValue = value.AsInterface(kind) + + if e.config.Timestamps { + expose.Timestamp = ×tamp + } + } + + var encodedLabels string + iter := record.Labels().Iter() + if iter.Len() > 0 { + encodedLabels = record.Labels().Encoded(e.config.LabelEncoder) + } + + var sb strings.Builder + + sb.WriteString(desc.Name()) + + if len(encodedLabels) > 0 || len(encodedResource) > 0 || len(encodedInstLabels) > 0 { + sb.WriteRune('{') + sb.WriteString(encodedResource) + if len(encodedInstLabels) > 0 && len(encodedResource) > 0 { + sb.WriteRune(',') + } + sb.WriteString(encodedInstLabels) + if len(encodedLabels) > 0 && (len(encodedInstLabels) > 0 || len(encodedResource) > 0) { + sb.WriteRune(',') + } + sb.WriteString(encodedLabels) + sb.WriteRune('}') + } + + expose.Name = sb.String() + + batch = append(batch, expose) + return nil + }) + if len(batch) == 0 { + return aggError + } + + data, err := e.marshal(batch) + if err != nil { + return err + } + fmt.Fprintln(e.config.Writer, string(data)) + + return aggError +} + +// marshal v with approriate indentation. +func (e *metricExporter) marshal(v interface{}) ([]byte, error) { + if e.config.PrettyPrint { + return json.MarshalIndent(v, "", "\t") + } + return json.Marshal(v) +} diff --git a/exporters/metric/stdout/stdout_test.go b/exporters/stdout/metric_test.go similarity index 77% rename from exporters/metric/stdout/stdout_test.go rename to exporters/stdout/metric_test.go index 582b7dab0ba..16839ae5e08 100644 --- a/exporters/metric/stdout/stdout_test.go +++ b/exporters/stdout/metric_test.go @@ -23,12 +23,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/api/kv" "go.opentelemetry.io/otel/api/metric" - "go.opentelemetry.io/otel/exporters/metric/stdout" "go.opentelemetry.io/otel/exporters/metric/test" + "go.opentelemetry.io/otel/exporters/stdout" export "go.opentelemetry.io/otel/sdk/export/metric" "go.opentelemetry.io/otel/sdk/export/metric/aggregation" "go.opentelemetry.io/otel/sdk/metric/aggregator/array" @@ -49,11 +50,11 @@ type testFixture struct { var testResource = resource.New(kv.String("R", "V")) -func newFixture(t *testing.T, config stdout.Config) testFixture { +func newFixture(t *testing.T, opts ...stdout.Option) testFixture { buf := &bytes.Buffer{} - config.Writer = buf - config.DoNotPrintTime = true - exp, err := stdout.NewRawExporter(config) + opts = append(opts, stdout.WithWriter(buf)) + opts = append(opts, stdout.WithoutTimestamps()) + exp, err := stdout.NewExporter(opts...) if err != nil { t.Fatal("Error building fixture: ", err) } @@ -77,19 +78,18 @@ func (fix testFixture) Export(checkpointSet export.CheckpointSet) { } func TestStdoutInvalidQuantile(t *testing.T) { - _, err := stdout.NewRawExporter(stdout.Config{ - Quantiles: []float64{1.1, 0.9}, - }) + _, err := stdout.NewExporter( + stdout.WithQuantiles([]float64{1.1, 0.9}), + ) require.Error(t, err, "Invalid quantile error expected") require.Equal(t, aggregation.ErrInvalidQuantile, err) } func TestStdoutTimestamp(t *testing.T) { var buf bytes.Buffer - exporter, err := stdout.NewRawExporter(stdout.Config{ - Writer: &buf, - DoNotPrintTime: false, - }) + exporter, err := stdout.NewExporter( + stdout.WithWriter(&buf), + ) if err != nil { t.Fatal("Invalid config: ", err) } @@ -114,35 +114,27 @@ func TestStdoutTimestamp(t *testing.T) { after := time.Now() - var printed map[string]interface{} - + var printed []interface{} if err := json.Unmarshal(buf.Bytes(), &printed); err != nil { t.Fatal("JSON parse error: ", err) } - updateTS := printed["time"].(string) - updateTimestamp, err := time.Parse(time.RFC3339Nano, updateTS) - if err != nil { - t.Fatal("JSON parse error: ", updateTS, ": ", err) - } - - lastValueTS := printed["updates"].([]interface{})[0].(map[string]interface{})["time"].(string) + require.Len(t, printed, 1) + lastValue, ok := printed[0].(map[string]interface{}) + require.True(t, ok, "last value format") + require.Contains(t, lastValue, "Timestamp") + lastValueTS := lastValue["Timestamp"].(string) lastValueTimestamp, err := time.Parse(time.RFC3339Nano, lastValueTS) if err != nil { t.Fatal("JSON parse error: ", lastValueTS, ": ", err) } - require.True(t, updateTimestamp.After(before)) - require.True(t, updateTimestamp.Before(after)) - - require.True(t, lastValueTimestamp.After(before)) - require.True(t, lastValueTimestamp.Before(after)) - - require.True(t, lastValueTimestamp.Before(updateTimestamp)) + assert.True(t, lastValueTimestamp.After(before)) + assert.True(t, lastValueTimestamp.Before(after)) } func TestStdoutCounterFormat(t *testing.T) { - fix := newFixture(t, stdout.Config{}) + fix := newFixture(t) checkpointSet := test.NewCheckpointSet(testResource) @@ -157,11 +149,11 @@ func TestStdoutCounterFormat(t *testing.T) { fix.Export(checkpointSet) - require.Equal(t, `{"updates":[{"name":"test.name{R=V,A=B,C=D}","sum":123}]}`, fix.Output()) + require.Equal(t, `[{"Name":"test.name{R=V,A=B,C=D}","Sum":123}]`, fix.Output()) } func TestStdoutLastValueFormat(t *testing.T) { - fix := newFixture(t, stdout.Config{}) + fix := newFixture(t) checkpointSet := test.NewCheckpointSet(testResource) @@ -175,11 +167,11 @@ func TestStdoutLastValueFormat(t *testing.T) { fix.Export(checkpointSet) - require.Equal(t, `{"updates":[{"name":"test.name{R=V,A=B,C=D}","last":123.456}]}`, fix.Output()) + require.Equal(t, `[{"Name":"test.name{R=V,A=B,C=D}","Last":123.456}]`, fix.Output()) } func TestStdoutMinMaxSumCount(t *testing.T) { - fix := newFixture(t, stdout.Config{}) + fix := newFixture(t) checkpointSet := test.NewCheckpointSet(testResource) @@ -195,13 +187,11 @@ func TestStdoutMinMaxSumCount(t *testing.T) { fix.Export(checkpointSet) - require.Equal(t, `{"updates":[{"name":"test.name{R=V,A=B,C=D}","min":123.456,"max":876.543,"sum":999.999,"count":2}]}`, fix.Output()) + require.Equal(t, `[{"Name":"test.name{R=V,A=B,C=D}","Min":123.456,"Max":876.543,"Sum":999.999,"Count":2}]`, fix.Output()) } func TestStdoutValueRecorderFormat(t *testing.T) { - fix := newFixture(t, stdout.Config{ - PrettyPrint: true, - }) + fix := newFixture(t, stdout.WithPrettyPrint()) checkpointSet := test.NewCheckpointSet(testResource) @@ -218,31 +208,29 @@ func TestStdoutValueRecorderFormat(t *testing.T) { fix.Export(checkpointSet) - require.Equal(t, `{ - "updates": [ - { - "name": "test.name{R=V,A=B,C=D}", - "min": 0.5, - "max": 999.5, - "sum": 500000, - "count": 1000, - "quantiles": [ - { - "q": 0.5, - "v": 500.5 - }, - { - "q": 0.9, - "v": 900.5 - }, - { - "q": 0.99, - "v": 990.5 - } - ] - } - ] -}`, fix.Output()) + require.Equal(t, `[ + { + "Name": "test.name{R=V,A=B,C=D}", + "Min": 0.5, + "Max": 999.5, + "Sum": 500000, + "Count": 1000, + "Quantiles": [ + { + "Quantile": 0.5, + "Value": 500.5 + }, + { + "Quantile": 0.9, + "Value": 900.5 + }, + { + "Quantile": 0.99, + "Value": 990.5 + } + ] + } +]`, fix.Output()) } func TestStdoutNoData(t *testing.T) { @@ -252,7 +240,7 @@ func TestStdoutNoData(t *testing.T) { t.Run(fmt.Sprintf("%T", agg), func(t *testing.T) { t.Parallel() - fix := newFixture(t, stdout.Config{}) + fix := newFixture(t) checkpointSet := test.NewCheckpointSet(testResource) @@ -262,7 +250,7 @@ func TestStdoutNoData(t *testing.T) { fix.Export(checkpointSet) - require.Equal(t, `{"updates":null}`, fix.Output()) + require.Equal(t, "", fix.Output()) }) } @@ -271,7 +259,7 @@ func TestStdoutNoData(t *testing.T) { } func TestStdoutLastValueNotSet(t *testing.T) { - fix := newFixture(t, stdout.Config{}) + fix := newFixture(t) checkpointSet := test.NewCheckpointSet(testResource) @@ -284,7 +272,7 @@ func TestStdoutLastValueNotSet(t *testing.T) { fix.Export(checkpointSet) - require.Equal(t, `{"updates":null}`, fix.Output()) + require.Equal(t, "", fix.Output()) } func TestStdoutResource(t *testing.T) { @@ -322,7 +310,7 @@ func TestStdoutResource(t *testing.T) { } for _, tc := range testCases { - fix := newFixture(t, stdout.Config{}) + fix := newFixture(t) checkpointSet := test.NewCheckpointSet(tc.res) @@ -336,6 +324,6 @@ func TestStdoutResource(t *testing.T) { fix.Export(checkpointSet) - require.Equal(t, `{"updates":[{"name":"test.name{`+tc.expect+`}","last":123.456}]}`, fix.Output()) + require.Equal(t, `[{"Name":"test.name{`+tc.expect+`}","Last":123.456}]`, fix.Output()) } } diff --git a/exporters/stdout/trace.go b/exporters/stdout/trace.go new file mode 100644 index 00000000000..af2d3bd81d9 --- /dev/null +++ b/exporters/stdout/trace.go @@ -0,0 +1,58 @@ +// 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 stdout + +import ( + "context" + "encoding/json" + "fmt" + + "go.opentelemetry.io/otel/sdk/export/trace" +) + +// Exporter is an implementation of trace.SpanSyncer that writes spans to stdout. +type traceExporter struct { + config Config +} + +// ExportSpan writes a SpanData in json format to stdout. +func (e *traceExporter) ExportSpan(ctx context.Context, data *trace.SpanData) { + if e.config.DisableTraceExport { + return + } + e.ExportSpans(ctx, []*trace.SpanData{data}) +} + +// ExportSpans writes SpanData in json format to stdout. +func (e *traceExporter) ExportSpans(ctx context.Context, data []*trace.SpanData) { + if e.config.DisableTraceExport || len(data) == 0 { + return + } + out, err := e.marshal(data) + if err != nil { + fmt.Fprintf(e.config.Writer, "error converting spanData to json: %v", err) + return + + } + fmt.Fprintln(e.config.Writer, string(out)) +} + +// marshal v with approriate indentation. +func (e *traceExporter) marshal(v interface{}) ([]byte, error) { + if e.config.PrettyPrint { + return json.MarshalIndent(v, "", "\t") + } + return json.Marshal(v) +} diff --git a/exporters/trace/stdout/stdout_test.go b/exporters/stdout/trace_test.go similarity index 95% rename from exporters/trace/stdout/stdout_test.go rename to exporters/stdout/trace_test.go index 3531bc2cce4..03078d25f33 100644 --- a/exporters/trace/stdout/stdout_test.go +++ b/exporters/stdout/trace_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package stdout +package stdout_test import ( "bytes" @@ -25,6 +25,7 @@ import ( "go.opentelemetry.io/otel/api/kv" "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/exporters/stdout" export "go.opentelemetry.io/otel/sdk/export/trace" "go.opentelemetry.io/otel/sdk/resource" ) @@ -32,7 +33,7 @@ import ( func TestExporter_ExportSpan(t *testing.T) { // write to buffer for testing var b bytes.Buffer - ex, err := NewExporter(Options{Writer: &b}) + ex, err := stdout.NewExporter(stdout.WithWriter(&b)) if err != nil { t.Errorf("Error constructing stdout exporter %s", err) } @@ -71,7 +72,7 @@ func TestExporter_ExportSpan(t *testing.T) { expectedSerializedNow, _ := json.Marshal(now) got := b.String() - expectedOutput := `{"SpanContext":{` + + expectedOutput := `[{"SpanContext":{` + `"TraceID":"0102030405060708090a0b0c0d0e0f10",` + `"SpanID":"0102030405060708","TraceFlags":0},` + `"ParentSpanID":"0000000000000000",` + @@ -126,7 +127,7 @@ func TestExporter_ExportSpan(t *testing.T) { `"InstrumentationLibrary":{` + `"Name":"",` + `"Version":""` + - `}}` + "\n" + `}}]` + "\n" if got != expectedOutput { t.Errorf("Want: %v but got: %v", expectedOutput, got) diff --git a/exporters/trace/stdout/stdout.go b/exporters/trace/stdout/stdout.go deleted file mode 100644 index 29f3d0eff04..00000000000 --- a/exporters/trace/stdout/stdout.go +++ /dev/null @@ -1,68 +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 stdout - -import ( - "context" - "encoding/json" - "io" - "os" - - export "go.opentelemetry.io/otel/sdk/export/trace" -) - -// Options are the options to be used when initializing a stdout export. -type Options struct { - // Writer is the destination. If not set, os.Stdout is used. - Writer io.Writer - - // PrettyPrint will pretty the json representation of the span, - // making it print "pretty". Default is false. - PrettyPrint bool -} - -// Exporter is an implementation of trace.SpanSyncer that writes spans to stdout. -type Exporter struct { - pretty bool - outputWriter io.Writer -} - -func NewExporter(o Options) (*Exporter, error) { - if o.Writer == nil { - o.Writer = os.Stdout - } - return &Exporter{ - pretty: o.PrettyPrint, - outputWriter: o.Writer, - }, nil -} - -// ExportSpan writes a SpanData in json format to stdout. -func (e *Exporter) ExportSpan(ctx context.Context, data *export.SpanData) { - var jsonSpan []byte - var err error - if e.pretty { - jsonSpan, err = json.MarshalIndent(data, "", "\t") - } else { - jsonSpan, err = json.Marshal(data) - } - if err != nil { - // ignore writer failures for now - _, _ = e.outputWriter.Write([]byte("Error converting spanData to json: " + err.Error())) - return - } - // ignore writer failures for now - _, _ = e.outputWriter.Write(append(jsonSpan, byte('\n'))) -} diff --git a/instrumentation/othttp/handler_example_test.go b/instrumentation/othttp/handler_example_test.go index b819da04727..c01c379ae01 100644 --- a/instrumentation/othttp/handler_example_test.go +++ b/instrumentation/othttp/handler_example_test.go @@ -24,12 +24,9 @@ import ( "go.opentelemetry.io/otel/api/kv" - "go.opentelemetry.io/otel/api/global" "go.opentelemetry.io/otel/api/trace" - mstdout "go.opentelemetry.io/otel/exporters/metric/stdout" - "go.opentelemetry.io/otel/exporters/trace/stdout" + "go.opentelemetry.io/otel/exporters/stdout" "go.opentelemetry.io/otel/instrumentation/othttp" - sdktrace "go.opentelemetry.io/otel/sdk/trace" ) func ExampleNewHandler() { @@ -48,29 +45,14 @@ func ExampleNewHandler() { */ // Write spans to stdout - exporter, err := stdout.NewExporter(stdout.Options{PrettyPrint: true}) + pusher, err := stdout.InstallNewPipeline([]stdout.Option{ + stdout.WithPrettyPrint(), + stdout.WithoutTimestamps(), // This makes the output deterministic + }, nil) if err != nil { log.Fatal(err) } - - tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), - sdktrace.WithSyncer(exporter)) - if err != nil { - log.Fatal(err) - } - - pusher, err := mstdout.NewExportPipeline(mstdout.Config{ - PrettyPrint: true, - DoNotPrintTime: true, // This makes the output deterministic - }) - - if err != nil { - log.Fatal(err) - } - - meterProvider := pusher.Provider() - global.SetTraceProvider(tp) - global.SetMeterProvider(meterProvider) + defer pusher.Stop() figureOutName := func(ctx context.Context, s string) (string, error) { pp := strings.SplitN(s, "/", 2) diff --git a/sdk/metric/example_test.go b/sdk/metric/example_test.go index a48b8dfb46c..28b1c0d40bb 100644 --- a/sdk/metric/example_test.go +++ b/sdk/metric/example_test.go @@ -15,41 +15,41 @@ package metric_test import ( + "bytes" "context" "fmt" "go.opentelemetry.io/otel/api/kv" "go.opentelemetry.io/otel/api/metric" - "go.opentelemetry.io/otel/exporters/metric/stdout" + "go.opentelemetry.io/otel/exporters/stdout" ) func ExampleNew() { - pusher, err := stdout.NewExportPipeline(stdout.Config{ - PrettyPrint: true, - DoNotPrintTime: true, // This makes the output deterministic - }) + buf := bytes.Buffer{} + _, pusher, err := stdout.NewExportPipeline([]stdout.Option{ + // Defaults to STDOUT. + stdout.WithWriter(&buf), + stdout.WithPrettyPrint(), + stdout.WithoutTimestamps(), // This makes the output deterministic + }, nil) if err != nil { panic(fmt.Sprintln("Could not initialize stdout exporter:", err)) } - defer pusher.Stop() - ctx := context.Background() + meter := metric.Must(pusher.Provider().Meter("example")) + counter := meter.NewInt64Counter("a.counter") + counter.Add(context.Background(), 100, kv.String("key", "value")) - key := kv.Key("key") - meter := pusher.Provider().Meter("example") - - counter := metric.Must(meter).NewInt64Counter("a.counter") - - counter.Add(ctx, 100, key.String("value")) + // Flush everything + pusher.Stop() + fmt.Println(buf.String()) // Output: - // { - // "updates": [ - // { - // "name": "a.counter{instrumentation.name=example,key=value}", - // "sum": 100 - // } - // ] - // } + // [ + // { + // "Name": "a.counter{instrumentation.name=example,key=value}", + // "Sum": 100 + // } + // ] }