diff --git a/exporter/stackdriverexporter/config.go b/exporter/stackdriverexporter/config.go new file mode 100644 index 000000000000..48e06647903c --- /dev/null +++ b/exporter/stackdriverexporter/config.go @@ -0,0 +1,26 @@ +// Copyright 2019, 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 stackdriverexporter + +import "github.com/open-telemetry/opentelemetry-service/config/configmodels" + +// Config defines configuration for Stackdriver exporter. +type Config struct { + configmodels.ExporterSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct. + ProjectID string `mapstructure:"project"` + EnableTracing bool `mapstructure:"enable_tracing"` + EnableMetrics bool `mapstructure:"enable_metrics"` + Prefix string `mapstructure:"metric_prefix"` +} diff --git a/exporter/stackdriverexporter/config_test.go b/exporter/stackdriverexporter/config_test.go new file mode 100644 index 000000000000..5aa0c87e6658 --- /dev/null +++ b/exporter/stackdriverexporter/config_test.go @@ -0,0 +1,52 @@ +// Copyright 2019, 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 stackdriverexporter + +import ( + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/open-telemetry/opentelemetry-service/config" + "github.com/open-telemetry/opentelemetry-service/config/configmodels" + "github.com/open-telemetry/opentelemetry-service/exporter" +) + +var _ = config.RegisterTestFactories() + +func TestLoadConfig(t *testing.T) { + + factory := exporter.GetFactory(typeStr) + + cfg, err := config.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml")) + + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Equal(t, len(cfg.Exporters), 2) + + r0 := cfg.Exporters["stackdriver"] + assert.Equal(t, r0, factory.CreateDefaultConfig()) + + r1 := cfg.Exporters["stackdriver/customname"].(*Config) + assert.Equal(t, r1.ExporterSettings, + configmodels.ExporterSettings{ + TypeVal: typeStr, + NameVal: "stackdriver/customname", + Enabled: true, + }) +} diff --git a/exporter/stackdriverexporter/factory.go b/exporter/stackdriverexporter/factory.go new file mode 100644 index 000000000000..634442c432b8 --- /dev/null +++ b/exporter/stackdriverexporter/factory.go @@ -0,0 +1,66 @@ +// Copyright 2019, 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 stackdriverexporter + +import ( + "github.com/open-telemetry/opentelemetry-service/config/configmodels" + "github.com/open-telemetry/opentelemetry-service/consumer" + "github.com/open-telemetry/opentelemetry-service/exporter" + "go.uber.org/zap" +) + +var _ = exporter.RegisterFactory(&exporterFactory{}) + +const ( + // The value of "type" key in configuration. + typeStr = "stackdriver" +) + +// exporterFactory is the factory for OpenCensus exporter. +type exporterFactory struct { +} + +// Type gets the type of the Exporter config created by this factory. +func (f *exporterFactory) Type() string { + return typeStr +} + +// CreateDefaultConfig creates the default configuration for exporter. +func (f *exporterFactory) CreateDefaultConfig() configmodels.Exporter { + return &Config{ + ExporterSettings: configmodels.ExporterSettings{ + TypeVal: typeStr, + NameVal: typeStr, + }, + } +} + +// CreateTraceExporter creates a trace exporter based on this config. +func (f *exporterFactory) CreateTraceExporter(logger *zap.Logger, cfg configmodels.Exporter) (consumer.TraceConsumer, exporter.StopFunc, error) { + eCfg := cfg.(*Config) + if !eCfg.EnableTracing { + return nil, nil, nil + } + return newStackdriverTraceExporter(eCfg.ProjectID, eCfg.Prefix) +} + +// CreateMetricsExporter creates a metrics exporter based on this config. +func (f *exporterFactory) CreateMetricsExporter(logger *zap.Logger, cfg configmodels.Exporter) (consumer.MetricsConsumer, exporter.StopFunc, error) { + eCfg := cfg.(*Config) + if !eCfg.EnableMetrics { + return nil, nil, nil + } + return newStackdriverMetricsExporter(eCfg.ProjectID, eCfg.Prefix) +} diff --git a/exporter/stackdriverexporter/factory_test.go b/exporter/stackdriverexporter/factory_test.go new file mode 100644 index 000000000000..48128abe5ab7 --- /dev/null +++ b/exporter/stackdriverexporter/factory_test.go @@ -0,0 +1,42 @@ +// Copyright 2019, 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 stackdriverexporter + +import ( + "testing" + + "go.uber.org/zap" + + "github.com/stretchr/testify/assert" + + "github.com/open-telemetry/opentelemetry-service/exporter" +) + +func TestCreateDefaultConfig(t *testing.T) { + factory := exporter.GetFactory(typeStr) + cfg := factory.CreateDefaultConfig() + assert.NotNil(t, cfg, "failed to create default config") +} + +func TestCreateExporter(t *testing.T) { + factory := exporter.GetFactory(typeStr) + cfg := factory.CreateDefaultConfig() + + _, _, err := factory.CreateTraceExporter(zap.NewNop(), cfg) + assert.Nil(t, err) + + _, _, err = factory.CreateMetricsExporter(zap.NewNop(), cfg) + assert.Nil(t, err) +} diff --git a/exporter/stackdriverexporter/stackdriver.go b/exporter/stackdriverexporter/stackdriver.go new file mode 100644 index 000000000000..97af14790142 --- /dev/null +++ b/exporter/stackdriverexporter/stackdriver.go @@ -0,0 +1,181 @@ +// Copyright 2019, 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 stackdriverexporter contains the wrapper for OpenTelemetry-Stackdriver +// exporter to be used in OpenTelemetry-Service. +package stackdriverexporter + +import ( + "context" + "fmt" + "sync" + "time" + + "contrib.go.opencensus.io/exporter/stackdriver" + "github.com/spf13/viper" + "go.opencensus.io/trace" + + "github.com/open-telemetry/opentelemetry-service/consumer" + "github.com/open-telemetry/opentelemetry-service/consumer/consumerdata" + "github.com/open-telemetry/opentelemetry-service/exporter/exporterwrapper" +) + +type stackdriverConfig struct { + ProjectID string `mapstructure:"project,omitempty"` + EnableTracing bool `mapstructure:"enable_tracing,omitempty"` + EnableMetrics bool `mapstructure:"enable_metrics,omitempty"` + MetricPrefix string `mapstructure:"metric_prefix,omitempty"` +} + +// TODO: Add metrics support to the exporterwrapper. +type stackdriverExporter struct { + exporter *stackdriver.Exporter +} + +var _ consumer.MetricsConsumer = (*stackdriverExporter)(nil) + +// StackdriverTraceExportersFromViper unmarshals the viper and returns an consumer.TraceConsumer targeting +// Stackdriver according to the configuration settings. +func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceConsumer, mps []consumer.MetricsConsumer, doneFns []func() error, err error) { + var cfg struct { + Stackdriver *stackdriverConfig `mapstructure:"stackdriver"` + } + if err := v.Unmarshal(&cfg); err != nil { + return nil, nil, nil, err + } + sc := cfg.Stackdriver + if sc == nil { + return nil, nil, nil, nil + } + if !sc.EnableTracing && !sc.EnableMetrics { + return nil, nil, nil, nil + } + + sde, serr := newStackdriverExporter(sc.ProjectID, sc.MetricPrefix) + + if serr != nil { + return nil, nil, nil, fmt.Errorf("Cannot configure Stackdriver exporter: %v", serr) + } + + exp := &stackdriverExporter{ + exporter: sde, + } + + sdte, err := exporterwrapper.NewExporterWrapper("stackdriver_trace", "ocservice.exporter.Stackdriver.ConsumeTraceData", sde) + if err != nil { + return nil, nil, nil, err + } + + // TODO: Examine "contrib.go.opencensus.io/exporter/stackdriver" to see + // if trace.ExportSpan was constraining and if perhaps the Stackdriver + // upload can use the context and information from the Node. + if sc.EnableTracing { + tps = append(tps, sdte) + } + + if sc.EnableMetrics { + mps = append(mps, exp) + } + + doneFns = append(doneFns, func() error { + sde.Flush() + return nil + }) + return +} + +func newStackdriverTraceExporter(ProjectID, MetricPrefix string) (consumer.TraceConsumer, func() error, error) { + sde, serr := newStackdriverExporter(ProjectID, MetricPrefix) + if serr != nil { + return nil, nil, fmt.Errorf("Cannot configure Stackdriver Trace exporter: %v", serr) + } + + tExp, err := exporterwrapper.NewExporterWrapper("stackdriver_trace", "ocservice.exporter.Stackdriver.ConsumeTraceData", sde) + if err != nil { + return nil, nil, err + } + // TODO: Examine "contrib.go.opencensus.io/exporter/stackdriver" to see + // if trace.ExportSpan was constraining and if perhaps the Stackdriver + // upload can use the context and information from the Node. + + doneFn := func() error { + sde.Flush() + return nil + } + + return tExp, doneFn, nil +} + +func newStackdriverMetricsExporter(ProjectID, MetricPrefix string) (consumer.MetricsConsumer, func() error, error) { + sde, serr := newStackdriverExporter(ProjectID, MetricPrefix) + if serr != nil { + return nil, nil, fmt.Errorf("Cannot configure Stackdriver metric exporter: %v", serr) + } + + mExp := &stackdriverExporter{ + exporter: sde, + } + + doneFn := func() error { + sde.Flush() + return nil + } + + return mExp, doneFn, nil +} + +func newStackdriverExporter(ProjectID, MetricPrefix string) (*stackdriver.Exporter, error) { + // TODO: For each ProjectID, create a different exporter + // or at least a unique Stackdriver client per ProjectID. + + return stackdriver.NewExporter(stackdriver.Options{ + // If the project ID is an empty string, it will be set by default based on + // the project this is running on in GCP. + ProjectID: ProjectID, + + MetricPrefix: MetricPrefix, + + // Stackdriver Metrics mandates a minimum of 60 seconds for + // reporting metrics. We have to enforce this as per the advisory + // at https://cloud.google.com/monitoring/custom-metrics/creating-metrics#writing-ts + // which says: + // + // "If you want to write more than one point to the same time series, then use a separate call + // to the timeSeries.create method for each point. Don't make the calls faster than one time per + // minute. If you are adding data points to different time series, then there is no rate limitation." + BundleDelayThreshold: 61 * time.Second, + }) +} + +func (sde *stackdriverExporter) ConsumeMetricsData(ctx context.Context, md consumerdata.MetricsData) error { + ctx, span := trace.StartSpan(ctx, + "opencensus.service.exporter.stackdriver.ExportMetricsData", + trace.WithSampler(trace.NeverSample())) + defer span.End() + + var setErrorOnce sync.Once + + err := sde.exporter.ExportMetricsProto(ctx, md.Node, md.Resource, md.Metrics) + if err != nil { + setErrorOnce.Do(func() { + span.SetStatus(trace.Status{Code: trace.StatusCodeInternal, Message: err.Error()}) + }) + + span.Annotate([]trace.Attribute{ + trace.StringAttribute("error", err.Error()), + }, "Error encountered") + } + + return nil +} diff --git a/exporter/stackdriverexporter/testdata/config.yaml b/exporter/stackdriverexporter/testdata/config.yaml new file mode 100644 index 000000000000..932c7125d4fa --- /dev/null +++ b/exporter/stackdriverexporter/testdata/config.yaml @@ -0,0 +1,21 @@ +receivers: + examplereceiver: + +processors: + exampleprocessor: + +exporters: + stackdriver: + stackdriver/customname: + enabled: true + project: my-project + enable_tracing: true + enable_metrics: true + metric_prefix: prefix + +pipelines: + traces: + receivers: [examplereceiver] + processors: [exampleprocessor] + exporters: [stackdriver] +