Repository: lightstep/otel-launcher-go
Pull request #191 (2022-06-22 19:27:45)

Document the alternate metrics SDK, usability fixes
**Description:** Document how to select the alternate metrics SDK and what changes when this is selected.

This includes two usability fixes discovered while testing the new configuration options here.

1. Out-of-range errors include the metric name
2. Validation errors sent specifically by Lightstep (via HTTP response headers) are printed nicely.

**Link to tracking Issue:** LS-29760

**Documentation:** See the two README changes here.


Patch:
diff --git a/README.md b/README.md
index c9856ec..d6df97f 100644
--- a/README.md
+++ b/README.md
@@ -52,21 +52,23 @@ Additional options
 
 ### Configuration Options
 
-|Config Option     |Env Variable      |Required|Default|
-|------------------|------------------|--------|-------|
-|WithServiceName            |LS_SERVICE_NAME                    |y       |-                               |
-|WithServiceVersion         |LS_SERVICE_VERSION                 |n       |unknown                         |
-|WithHeaders                |OTEL_EXPORTER_OTLP_HEADERS         |n       |{}                              |
-|WithSpanExporterEndpoint   |OTEL_EXPORTER_OTLP_SPAN_ENDPOINT   |n       |ingest.lightstep.com:443        |
-|WithSpanExporterInsecure   |OTEL_EXPORTER_OTLP_SPAN_INSECURE   |n       |false                           |
-|WithMetricExporterEndpoint |OTEL_EXPORTER_OTLP_METRIC_ENDPOINT |n       |ingest.lightstep.com:443        |
-|WithMetricExporterInsecure |OTEL_EXPORTER_OTLP_METRIC_INSECURE |n       |false                           |
-|WithAccessToken            |LS_ACCESS_TOKEN                    |n       |-                               |
-|WithLogLevel               |OTEL_LOG_LEVEL                     |n       |info                            |
-|WithPropagators            |OTEL_PROPAGATORS                   |n       |b3                              |
-|WithResourceAttributes     |OTEL_RESOURCE_ATTRIBUTES           |n       |-                               |
-|WithMetricReportingPeriod  |OTEL_EXPORTER_OTLP_METRIC_PERIOD   |n       |30s                             |
-|WithMetricsEnabled         |LS_METRICS_ENABLED                 |n       |True                            |
+| Config Option                           | Env Variable                                     | Required | Default                  |
+|-----------------------------------------|--------------------------------------------------|----------|--------------------------|
+| WithServiceName                         | LS_SERVICE_NAME                                  | y        | -                        |
+| WithServiceVersion                      | LS_SERVICE_VERSION                               | n        | unknown                  |
+| WithHeaders                             | OTEL_EXPORTER_OTLP_HEADERS                       | n        | {}                       |
+| WithSpanExporterEndpoint                | OTEL_EXPORTER_OTLP_SPAN_ENDPOINT                 | n        | ingest.lightstep.com:443 |
+| WithSpanExporterInsecure                | OTEL_EXPORTER_OTLP_SPAN_INSECURE                 | n        | false                    |
+| WithMetricExporterEndpoint              | OTEL_EXPORTER_OTLP_METRIC_ENDPOINT               | n        | ingest.lightstep.com:443 |
+| WithMetricExporterInsecure              | OTEL_EXPORTER_OTLP_METRIC_INSECURE               | n        | false                    |
+| WithMetricExporterTemporalityPreference | OTEL_EXPORTER_OTLP_METRIC_TEMPORALITY_PREFERENCE | n        | false                    |
+| WithAccessToken                         | LS_ACCESS_TOKEN                                  | n        | -                        |
+| WithLogLevel                            | OTEL_LOG_LEVEL                                   | n        | info                     |
+| WithPropagators                         | OTEL_PROPAGATORS                                 | n        | b3                       |
+| WithResourceAttributes                  | OTEL_RESOURCE_ATTRIBUTES                         | n        | -                        |
+| WithMetricReportingPeriod               | OTEL_EXPORTER_OTLP_METRIC_PERIOD                 | n        | 30s                      |
+| WithMetricsEnabled                      | LS_METRICS_ENABLED                               | n        | True                     |
+| WithLightstepMetricsSDK                 | LS_METRICS_SDK                                   | n        | False                    |
 
 ### Principles behind Launcher
 
@@ -80,6 +82,57 @@ Another decision we made with launcher is to provide end users with a layer of v
 
 Start using it today in [Go](https://github.com/lightstep/otel-launcher-go), [Java](https://github.com/lightstep/otel-launcher-java), [Javascript](https://github.com/lightstep/otel-launcher-node) and [Python](https://github.com/lightstep/otel-launcher-python) and let us know what you think!
 
+### Lightstep Metrics SDK
+
+The Launcher contains an alternative to the [OTel-Go community Metrics
+SDK](https://github.com/open-telemetry/opentelemetry-go) being
+maintained by Lightstep as a way to quickly validate newer
+OpenTelemetry features, such as the OpenTelemetry exponential
+histogram.
+
+This Metrics SDK is being used in production at Lightstep.
+
+To select the alternative Metrics SDK, use
+`WithLightstepMetricsSDK(true)` or set `LS_METRICS_SDK=true`.
+
+The differences between the OpenTelemetry Metrics SDK specification
+and the alternative SDK are documented in its
+[README](./lightstep/sdk/metric/README.md).
+
+### Temporality settings
+
+OpenTelemetry metrics SDKs give the user control over "temporality",
+which is the selection of "delta" or "cumulative" policies for
+aggregating Counter and Histogram instruments.  These settings determine
+both memory usage and reliability of metrics reporting.
+
+Delta temporality requires less memory than cumulative temporality for
+synchronous instruments, while Cumulative requires less memory than
+delta temporality for asynchronous instruments.  When reporting is
+intermittent, cumulative series will average out the missing reports,
+whereas delta series will have gaps.
+
+Note that Lightstep considers a change of temporality to be a breaking
+change.  Once a temporality preference has been set, the setting has
+to be maintained.  The temporality preference is configured by calling
+`WithMetricExporterTemporalityPreference()` or using the
+`OTEL_EXPORTER_OTLP_METRIC_TEMPORALITY_PREFERENCE` environment
+variable.
+
+The exporter temporality preference is set to "cumulative" by default,
+as per the OpenTelemetry SDK specification.  The OpenTelemetry
+specified "delta" temporality preference is not recommended for
+Lightstep users.
+
+The launcher supports an experimental "stateless" temporality
+preference.  This selection configures the ideal behavior for
+Lightstep by mixing temporality setings.  This setting uses delta
+temporality for synchronous Counter and Histogram instruments, while
+using cumulative temporality for asynchronous Counters.  Note that
+synchronous and asynchronous UpDownCounter instruments are specified
+to use cumulative temporality in OpenTelemetry metrics SDKs
+independent of the temporality preference.
+
 ------
 
 *Made with* ![:heart:](https://a.slack-edge.com/production-standard-emoji-assets/10.2/apple-medium/2764-fe0f.png) *@ [Lightstep](http://lightstep.com/)*
diff --git a/examples/metrics/metrics.go b/examples/metrics/metrics.go
index 5fed660..61edf4e 100644
--- a/examples/metrics/metrics.go
+++ b/examples/metrics/metrics.go
@@ -55,6 +55,11 @@ func main() {
 	c1, _ := meter.SyncInt64().Counter(prefix + "counter")
 	c2, _ := meter.SyncInt64().UpDownCounter(prefix + "updowncounter")
 	hist, _ := meter.SyncFloat64().Histogram(prefix + "histogram")
+	mmsc, _ := meter.SyncFloat64().Histogram(prefix + "mmsc",
+		instrument.WithDescription(`{
+  "aggregation": "minmaxsumcount"
+}`),
+	)
 	go func() {
 		for {
 			c1.Add(ctx, 1)
@@ -65,7 +70,9 @@ func main() {
 			mult *= mult
 
 			for i := 0; i < 10000; i++ {
-				hist.Record(ctx, mult*(100+rand.NormFloat64()*100))
+				value := math.Abs(mult*(100+rand.NormFloat64()*100))
+				hist.Record(ctx, value)
+				mmsc.Record(ctx, value)
 			}
 
 			time.Sleep(time.Second)
diff --git a/launcher/config.go b/launcher/config.go
index d79fc9d..c5d732a 100644
--- a/launcher/config.go
+++ b/launcher/config.go
@@ -144,9 +144,9 @@ func WithMetricReportingPeriod(p time.Duration) Option {
 // WithMetricTemporalityPreference controls the temporality preference
 // used for Counter and Histogram (only not for UpDownCounter, which
 // ignores this preference for specified reasons).
-func WithMetricTemporalityPreference(prefName string) Option {
+func WithMetricExporterTemporalityPreference(prefName string) Option {
 	return func(c *Config) {
-		c.MetricTemporalityPreference = prefName
+		c.MetricExporterTemporalityPreference = prefName
 	}
 }
 
@@ -168,6 +168,14 @@ func WithLogger(logger Logger) Option {
 	}
 }
 
+// WithLightstepMetricsSDK enables the Lightstep metrics SDK from this
+// repository.
+func WithLightstepMetricsSDK(alt bool) Option {
+	return func(c *Config) {
+		c.UseLightstepMetricsSDK = alt
+	}
+}
+
 type DefaultLogger struct {
 }
 
@@ -197,22 +205,23 @@ const (
 )
 
 type Config struct {
-	SpanExporterEndpoint           string            `env:"OTEL_EXPORTER_OTLP_SPAN_ENDPOINT,default=ingest.lightstep.com:443"`
-	SpanExporterEndpointInsecure   bool              `env:"OTEL_EXPORTER_OTLP_SPAN_INSECURE,default=false"`
-	ServiceName                    string            `env:"LS_SERVICE_NAME"`
-	ServiceVersion                 string            `env:"LS_SERVICE_VERSION,default=unknown"`
-	Headers                        map[string]string `env:"OTEL_EXPORTER_OTLP_HEADERS"`
-	MetricExporterEndpoint         string            `env:"OTEL_EXPORTER_OTLP_METRIC_ENDPOINT,default=ingest.lightstep.com:443"`
-	MetricExporterEndpointInsecure bool              `env:"OTEL_EXPORTER_OTLP_METRIC_INSECURE,default=false"`
-	MetricsEnabled                 bool              `env:"LS_METRICS_ENABLED,default=true"`
-	LogLevel                       string            `env:"OTEL_LOG_LEVEL,default=info"`
-	Propagators                    []string          `env:"OTEL_PROPAGATORS,default=b3"`
-	MetricReportingPeriod          string            `env:"OTEL_EXPORTER_OTLP_METRIC_PERIOD,default=30s"`
-	MetricTemporalityPreference    string            `env:"OTEL_EXPORTER_OTLP_METRIC_TEMPORALITY_PREFERENCE,default=cumulative"`
-	ResourceAttributes             map[string]string
-	Resource                       *resource.Resource
-	logger                         Logger
-	errorHandler                   otel.ErrorHandler
+	SpanExporterEndpoint                string            `env:"OTEL_EXPORTER_OTLP_SPAN_ENDPOINT,default=ingest.lightstep.com:443"`
+	SpanExporterEndpointInsecure        bool              `env:"OTEL_EXPORTER_OTLP_SPAN_INSECURE,default=false"`
+	ServiceName                         string            `env:"LS_SERVICE_NAME"`
+	ServiceVersion                      string            `env:"LS_SERVICE_VERSION,default=unknown"`
+	Headers                             map[string]string `env:"OTEL_EXPORTER_OTLP_HEADERS"`
+	MetricExporterEndpoint              string            `env:"OTEL_EXPORTER_OTLP_METRIC_ENDPOINT,default=ingest.lightstep.com:443"`
+	MetricExporterEndpointInsecure      bool              `env:"OTEL_EXPORTER_OTLP_METRIC_INSECURE,default=false"`
+	MetricExporterTemporalityPreference string            `env:"OTEL_EXPORTER_OTLP_METRIC_TEMPORALITY_PREFERENCE,default=cumulative"`
+	MetricsEnabled                      bool              `env:"LS_METRICS_ENABLED,default=true"`
+	LogLevel                            string            `env:"OTEL_LOG_LEVEL,default=info"`
+	Propagators                         []string          `env:"OTEL_PROPAGATORS,default=b3"`
+	MetricReportingPeriod               string            `env:"OTEL_EXPORTER_OTLP_METRIC_PERIOD,default=30s"`
+	UseLightstepMetricsSDK              bool              `env:"LS_METRICS_SDK,default=false"`
+	ResourceAttributes                  map[string]string
+	Resource                            *resource.Resource
+	logger                              Logger
+	errorHandler                        otel.ErrorHandler
 }
 
 func checkEndpointDefault(value, defValue string) error {
@@ -371,12 +380,13 @@ func setupMetrics(c Config) (func() error, error) {
 		return nil, nil
 	}
 	return pipelines.NewMetricsPipeline(pipelines.PipelineConfig{
-		Endpoint:              c.MetricExporterEndpoint,
-		Insecure:              c.MetricExporterEndpointInsecure,
-		Headers:               c.Headers,
-		Resource:              c.Resource,
-		ReportingPeriod:       c.MetricReportingPeriod,
-		TemporalityPreference: c.MetricTemporalityPreference,
+		Endpoint:               c.MetricExporterEndpoint,
+		Insecure:               c.MetricExporterEndpointInsecure,
+		Headers:                c.Headers,
+		Resource:               c.Resource,
+		ReportingPeriod:        c.MetricReportingPeriod,
+		TemporalityPreference:  c.MetricExporterTemporalityPreference,
+		UseLightstepMetricsSDK: c.UseLightstepMetricsSDK,
 	})
 }
 
diff --git a/lightstep/sdk/metric/README.md b/lightstep/sdk/metric/README.md
new file mode 100644
index 0000000..508a241
--- /dev/null
+++ b/lightstep/sdk/metric/README.md
@@ -0,0 +1,66 @@
+## Lightstep implementation of the OpenTelemetry-Go Metrics SDK
+
+This implementation began as a prototype implementation of the
+OpenTelemetry SDK specification and has been used as a reference point
+for the OpenTelemetry-Go community metrics SDK.  Lightstep's internal
+code base is largely written in Go and has a number of specific
+requirements which made a direct upgrade to the OpenTelemetry-Go
+community Metrics SDK difficult.
+
+Instead of waiting for the OpenTelemetry-Go community SDK to surpass
+the current OpenTelemetry specification, to support our requirements,
+we decided to enable the experimental features in an SDK we control.
+
+This implementation of the OpenTelemetry SDK has been made public,
+however it is not covered by any stability guarantee.
+
+Differences from the OpenTelemetry metrics SDK specification:
+
+1. The exponential histogram is enabled by default; the
+   explicit-boundary histogram has been removed
+2. The OTLP exporter is the only provided exporter
+3. There is an alternate aggregation for Histogram instruments,
+   "MinMaxSumCount", which encodes as a zero-bucket explicit-boundary
+   histogram.
+
+Lightstep expects to continue maintaining this implementation until
+the community SDK supports configuring the behaviors listed above.
+Moreover, Lightstep expects to make several optimizations in this SDK
+to further optimize the synchronous instrument fast path and continue
+improving memory performance.
+
+### Metric instrument "Hints" API
+
+There is a standing feature request in OpenTelemetry for a "Hints" API
+to inform the SDK of recommended aggregations in the source, when
+registering instruments.  This SDK implements an experimental form of
+Hints API, described as follows.
+
+The Views implementation attempts to parse the Description of each
+metric instrument as the JSON-encoded form of a
+`(github.com/lightstep/otel-launcher-go/lightstep/sdk/metric/view).Hint`
+structure.  If successfully parsed, the embedded aggrgation kind and
+configuration will be used, and the embedded Description field
+replaces the original hint.
+
+For example, to set the number of exponential histogram buckets, use a
+description like this:
+
+```
+{
+  "description": "measurement of ...",
+  "config": {
+    "histogram": {
+      "max_size": 320
+    }
+  }
+}
+```
+
+To set the MinMaxSumCount aggregation for a specific histogram instrument:
+
+```
+{
+  "description": "measurement of ...",
+  "aggregation": "minmaxsumcount"
+}
diff --git a/lightstep/sdk/metric/aggregator/aggregator.go b/lightstep/sdk/metric/aggregator/aggregator.go
index 753a5d2..0d9f47a 100644
--- a/lightstep/sdk/metric/aggregator/aggregator.go
+++ b/lightstep/sdk/metric/aggregator/aggregator.go
@@ -34,25 +34,25 @@ var (
 // This rejects NaN and Inf values.  This rejects negative values when the
 // aggregation does not support negative values, including
 // monotonic counter metrics and Histogram metrics.
-func RangeTest[N number.Any, Traits number.Traits[N]](num N, kind sdkinstrument.Kind) bool {
+func RangeTest[N number.Any, Traits number.Traits[N]](num N, desc sdkinstrument.Descriptor) bool {
 	var traits Traits
 
 	if traits.IsInf(num) {
-		otel.Handle(ErrInfInput)
+		otel.Handle(fmt.Errorf("%s: %w", desc.Name, ErrInfInput))
 		return false
 	}
 
 	if traits.IsNaN(num) {
-		otel.Handle(ErrNaNInput)
+		otel.Handle(fmt.Errorf("%s: %w", desc.Name, ErrNaNInput))
 		return false
 	}
 
 	// Check for negative values
-	switch kind {
+	switch desc.Kind {
 	case sdkinstrument.SyncCounter,
 		sdkinstrument.SyncHistogram:
 		if num < 0 {
-			otel.Handle(ErrNegativeInput)
+			otel.Handle(fmt.Errorf("%s: %w", desc.Name, ErrNegativeInput))
 			return false
 		}
 	}
diff --git a/lightstep/sdk/metric/internal/asyncstate/async.go b/lightstep/sdk/metric/internal/asyncstate/async.go
index 324c779..22b1ca6 100644
--- a/lightstep/sdk/metric/internal/asyncstate/async.go
+++ b/lightstep/sdk/metric/internal/asyncstate/async.go
@@ -149,7 +149,7 @@ func capture[N number.Any, Traits number.Traits[N]](ctx context.Context, inst *I
 		return
 	}
 
-	if !aggregator.RangeTest[N, Traits](value, inst.descriptor.Kind) {
+	if !aggregator.RangeTest[N, Traits](value, inst.descriptor) {
 		return
 	}
 
diff --git a/lightstep/sdk/metric/internal/syncstate/sync.go b/lightstep/sdk/metric/internal/syncstate/sync.go
index b481197..c95b0fe 100644
--- a/lightstep/sdk/metric/internal/syncstate/sync.go
+++ b/lightstep/sdk/metric/internal/syncstate/sync.go
@@ -157,7 +157,7 @@ func capture[N number.Any, Traits number.Traits[N]](_ context.Context, inst *Ins
 
 	// Note: Here, this is the place to use context, e.g., extract baggage.
 
-	if !aggregator.RangeTest[N, Traits](num, inst.descriptor.Kind) {
+	if !aggregator.RangeTest[N, Traits](num, inst.descriptor) {
 		return
 	}
 
diff --git a/pipelines/common.go b/pipelines/common.go
index 8c5d1fd..8e33537 100644
--- a/pipelines/common.go
+++ b/pipelines/common.go
@@ -36,9 +36,9 @@ type PipelineConfig struct {
 	// Credentials carries the TLS settings.
 	Credentials credentials.TransportCredentials
 
-	// UseAlternateMetricsSDK determines whether to use the metrics
+	// UseLightstepMetricsSDK determines whether to use the metrics
 	// SDK at ../lightstep/sdk/metric.
-	UseAlternateMetricsSDK bool
+	UseLightstepMetricsSDK bool
 }
 
 type PipelineSetupFunc func(PipelineConfig) (func() error, error)
diff --git a/pipelines/metrics.go b/pipelines/metrics.go
index f8cdeaf..2a7fb97 100644
--- a/pipelines/metrics.go
+++ b/pipelines/metrics.go
@@ -16,7 +16,9 @@ package pipelines
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"strconv"
 	"strings"
 	"time"
 
@@ -28,6 +30,7 @@ import (
 
 	hostMetrics "go.opentelemetry.io/contrib/instrumentation/host"
 	runtimeMetrics "go.opentelemetry.io/contrib/instrumentation/runtime"
+	"go.opentelemetry.io/otel"
 
 	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
 	"go.opentelemetry.io/otel/metric"
@@ -36,10 +39,13 @@ import (
 	// The old Metrics SDK
 	oldotlpmetric "go.opentelemetry.io/otel/exporters/otlp/otlpmetric"
 	controller "go.opentelemetry.io/otel/sdk/metric/controller/basic"
+	oldaggregation "go.opentelemetry.io/otel/sdk/metric/export/aggregation"
 	processor "go.opentelemetry.io/otel/sdk/metric/processor/basic"
 	selector "go.opentelemetry.io/otel/sdk/metric/selector/simple"
 
+	"google.golang.org/grpc"
 	"google.golang.org/grpc/encoding/gzip"
+	"google.golang.org/grpc/metadata"
 )
 
 func NewMetricsPipeline(c PipelineConfig) (func() error, error) {
@@ -59,23 +65,23 @@ func NewMetricsPipeline(c PipelineConfig) (func() error, error) {
 	var provider metric.MeterProvider
 	var shutdown func() error
 
-	if c.UseAlternateMetricsSDK {
-		// Install the Lightstep alternate metrics SDK
+	newPref, oldPref, err := tempoOptions(c)
+	if err != nil {
+		return nil, fmt.Errorf("invalid metric view configuration: %v", err)
+	}
+
+	if c.UseLightstepMetricsSDK {
+		// Install the Lightstep metrics SDK
 		metricExporter, err := c.newMetricsExporter()
 		if err != nil {
 			return nil, fmt.Errorf("failed to create metric exporter: %v", err)
 		}
 
-		vopts, err := viewOptions(c)
-		if err != nil {
-			return nil, fmt.Errorf("invalid metric view configuration: %v", err)
-		}
-
 		sdk := sdkmetric.NewMeterProvider(
 			sdkmetric.WithResource(c.Resource),
 			sdkmetric.WithReader(
 				sdkmetric.NewPeriodicReader(metricExporter, period),
-				vopts...,
+				newPref,
 			),
 		)
 
@@ -86,7 +92,7 @@ func NewMetricsPipeline(c PipelineConfig) (func() error, error) {
 
 	} else {
 		// Install the OTel-Go community metrics SDK.
-		metricExporter, err := c.newOldMetricsExporter()
+		metricExporter, err := c.newOldMetricsExporter(oldPref)
 		if err != nil {
 			return nil, fmt.Errorf("failed to create metric exporter: %v", err)
 		}
@@ -122,60 +128,134 @@ func NewMetricsPipeline(c PipelineConfig) (func() error, error) {
 	return shutdown, nil
 }
 
+var errNoSingleCount = fmt.Errorf("no count")
+
+func singleCount(values []string) (int, error) {
+	if len(values) != 1 {
+		return 0, errNoSingleCount
+	}
+	return strconv.Atoi(values[0])
+}
+
+type dropExample struct {
+	Reason string   `json:"reason"`
+	Names  []string `json:"names"`
+}
+
+type dropSummary struct {
+	Dropped struct {
+		Points  int `json:"points,omitempty"`
+		Metrics int `json:"metrics,omitempty"`
+	} `json:"dropped"`
+	Examples []dropExample `json:"examples,omitempty"`
+}
+
+func interceptor(
+	ctx context.Context,
+	method string,
+	req, reply interface{},
+	cc *grpc.ClientConn,
+	invoker grpc.UnaryInvoker,
+	opts ...grpc.CallOption,
+) error {
+	const invalidTrailerPrefix = "otlp-invalid-"
+
+	var md metadata.MD
+	err := invoker(ctx, method, req, reply, cc, append(opts, grpc.Trailer(&md))...)
+	if err == nil && md != nil {
+		var ds dropSummary
+		for key, values := range md {
+			key = strings.ToLower(key)
+			if !strings.HasPrefix(key, "otlp-") {
+				continue
+			}
+
+			if key == "otlp-points-dropped" {
+				if points, err := singleCount(values); err == nil {
+					ds.Dropped.Points = points
+				}
+			} else if key == "otlp-metrics-dropped" {
+				if metrics, err := singleCount(values); err == nil {
+					ds.Dropped.Metrics = metrics
+				}
+			} else if strings.HasPrefix(key, invalidTrailerPrefix) {
+				key = key[len(invalidTrailerPrefix):]
+				key = strings.ReplaceAll(key, "-", " ")
+				ds.Examples = append(ds.Examples, dropExample{
+					Reason: key,
+					Names:  values,
+				})
+			}
+		}
+		data, _ := json.Marshal(ds)
+		otel.Handle(fmt.Errorf("metrics partial failure: %v", string(data)))
+	}
+	return err
+}
+
+func (c PipelineConfig) newClient() otlpmetric.Client {
+	return otlpmetricgrpc.NewClient(
+		c.secureMetricOption(),
+		otlpmetricgrpc.WithEndpoint(c.Endpoint),
+		otlpmetricgrpc.WithHeaders(c.Headers),
+		otlpmetricgrpc.WithCompressor(gzip.Name),
+		otlpmetricgrpc.WithDialOption(
+			grpc.WithUnaryInterceptor(interceptor),
+		),
+	)
+}
+
 func (c PipelineConfig) newMetricsExporter() (*otlpmetric.Exporter, error) {
 	return otlpmetric.New(
 		context.Background(),
-		otlpmetricgrpc.NewClient(
-			c.secureMetricOption(),
-			otlpmetricgrpc.WithEndpoint(c.Endpoint),
-			otlpmetricgrpc.WithHeaders(c.Headers),
-			otlpmetricgrpc.WithCompressor(gzip.Name),
-		),
+		c.newClient(),
 	)
 }
 
-func (c PipelineConfig) newOldMetricsExporter() (*oldotlpmetric.Exporter, error) {
+func (c PipelineConfig) newOldMetricsExporter(tempo oldaggregation.TemporalitySelector) (*oldotlpmetric.Exporter, error) {
 	return oldotlpmetric.New(
 		context.Background(),
-		otlpmetricgrpc.NewClient(
-			c.secureMetricOption(),
-			otlpmetricgrpc.WithEndpoint(c.Endpoint),
-			otlpmetricgrpc.WithHeaders(c.Headers),
-			otlpmetricgrpc.WithCompressor(gzip.Name),
-		),
+		c.newClient(),
+		oldotlpmetric.WithMetricAggregationTemporalitySelector(tempo),
 	)
 }
 
-func viewOptions(c PipelineConfig) ([]view.Option, error) {
+func tempoOptions(c PipelineConfig) (view.Option, oldaggregation.TemporalitySelector, error) {
 	syncPref := aggregation.CumulativeTemporality
 	asyncPref := aggregation.CumulativeTemporality
+	var oldSelector oldaggregation.TemporalitySelector
 
 	switch lower := strings.ToLower(c.TemporalityPreference); lower {
 	case "delta":
 		// Delta means exercising the cumulative-to-delta
 		// export path.  This is an unusual setting for
-		// Lightstep users to choose, but could be
+		// Lightstep users to choose.
 		syncPref = aggregation.DeltaTemporality
 		asyncPref = aggregation.DeltaTemporality
+
+		oldSelector = oldaggregation.DeltaTemporalitySelector()
 	case "stateless":
+		// asyncPref set above.
 		syncPref = aggregation.DeltaTemporality
+
+		oldSelector = oldaggregation.StatelessTemporalitySelector()
 	case "", "cumulative":
-		// Defaults set above.
+		// syncPref, asyncPref set above.
+		oldSelector = oldaggregation.CumulativeTemporalitySelector()
 	default:
-		return nil, fmt.Errorf("invalid temporality preference: %v", c.TemporalityPreference)
+		return nil, nil, fmt.Errorf("invalid temporality preference: %v", c.TemporalityPreference)
+
 	}
-	return []view.Option{
-		view.WithDefaultAggregationTemporalitySelector(
-			func(k sdkinstrument.Kind) aggregation.Temporality {
-				switch k {
-				case sdkinstrument.SyncUpDownCounter, sdkinstrument.AsyncUpDownCounter:
-					return aggregation.CumulativeTemporality
-				case sdkinstrument.SyncCounter, sdkinstrument.SyncHistogram:
-					return syncPref
-				default:
-					return asyncPref
-				}
-			},
-		),
-	}, nil
+	return view.WithDefaultAggregationTemporalitySelector(
+		func(k sdkinstrument.Kind) aggregation.Temporality {
+			switch k {
+			case sdkinstrument.SyncUpDownCounter, sdkinstrument.AsyncUpDownCounter:
+				return aggregation.CumulativeTemporality
+			case sdkinstrument.SyncCounter, sdkinstrument.SyncHistogram:
+				return syncPref
+			default:
+				return asyncPref
+			}
+		},
+	), oldSelector, nil
 }


Test patch:
diff --git a/launcher/config_test.go b/launcher/config_test.go
index f4866fb..0295082 100644
--- a/launcher/config_test.go
+++ b/launcher/config_test.go
@@ -23,11 +23,13 @@ import (
 	"testing"
 	"time"
 
+	sdkmetric "github.com/lightstep/otel-launcher-go/lightstep/sdk/metric"
 	"github.com/lightstep/otel-launcher-go/pipelines/test"
 	"github.com/stretchr/testify/suite"
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/baggage"
+	metricglobal "go.opentelemetry.io/otel/metric/global"
 	"go.opentelemetry.io/otel/sdk/resource"
 	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
 )
@@ -365,20 +367,21 @@ func (suite *testSuite) TestDefaultConfig() {
 	}
 
 	expected := Config{
-		ServiceName:                    "",
-		ServiceVersion:                 "unknown",
-		SpanExporterEndpoint:           "ingest.lightstep.com:443",
-		SpanExporterEndpointInsecure:   false,
-		MetricExporterEndpoint:         "ingest.lightstep.com:443",
-		MetricExporterEndpointInsecure: false,
-		MetricReportingPeriod:          "30s",
-		MetricsEnabled:                 true,
-		MetricTemporalityPreference:    "cumulative",
-		LogLevel:                       "info",
-		Propagators:                    []string{"b3"},
-		Resource:                       resource.NewWithAttributes(semconv.SchemaURL, attributes...),
-		logger:                         &suite.testLogger,
-		errorHandler:                   &suite.testErrorHandler,
+		ServiceName:                         "",
+		ServiceVersion:                      "unknown",
+		SpanExporterEndpoint:                "ingest.lightstep.com:443",
+		SpanExporterEndpointInsecure:        false,
+		MetricExporterEndpoint:              "ingest.lightstep.com:443",
+		MetricExporterEndpointInsecure:      false,
+		MetricReportingPeriod:               "30s",
+		MetricsEnabled:                      true,
+		MetricExporterTemporalityPreference: "cumulative",
+		LogLevel:                            "info",
+		Propagators:                         []string{"b3"},
+		Resource:                            resource.NewWithAttributes(semconv.SchemaURL, attributes...),
+		UseLightstepMetricsSDK:              false,
+		logger:                              &suite.testLogger,
+		errorHandler:                        &suite.testErrorHandler,
 	}
 	assert.Equal(expected, config)
 }
@@ -403,19 +406,20 @@ func (suite *testSuite) TestEnvironmentVariables() {
 	}
 
 	expected := Config{
-		ServiceName:                    "test-service-name",
-		ServiceVersion:                 "test-service-version",
-		SpanExporterEndpoint:           "satellite-url",
-		SpanExporterEndpointInsecure:   true,
-		MetricExporterEndpoint:         "metrics-url",
-		MetricExporterEndpointInsecure: true,
-		MetricReportingPeriod:          "30s",
-		MetricTemporalityPreference:    "delta",
-		LogLevel:                       "debug",
-		Propagators:                    []string{"b3", "w3c"},
-		Resource:                       resource.NewWithAttributes(semconv.SchemaURL, attributes...),
-		logger:                         &suite.testLogger,
-		errorHandler:                   &suite.testErrorHandler,
+		ServiceName:                         "test-service-name",
+		ServiceVersion:                      "test-service-version",
+		SpanExporterEndpoint:                "satellite-url",
+		SpanExporterEndpointInsecure:        true,
+		MetricExporterEndpoint:              "metrics-url",
+		MetricExporterEndpointInsecure:      true,
+		MetricReportingPeriod:               "30s",
+		MetricExporterTemporalityPreference: "delta",
+		LogLevel:                            "debug",
+		Propagators:                         []string{"b3", "w3c"},
+		Resource:                            resource.NewWithAttributes(semconv.SchemaURL, attributes...),
+		UseLightstepMetricsSDK:              true,
+		logger:                              &suite.testLogger,
+		errorHandler:                        &suite.testErrorHandler,
 	}
 	assert.Equal(expected, config)
 
@@ -434,11 +438,12 @@ func (suite *testSuite) TestConfigurationOverrides() {
 		WithSpanExporterInsecure(false),
 		WithMetricExporterEndpoint("override-metrics-url"),
 		WithMetricExporterInsecure(false),
-		WithMetricTemporalityPreference("stateless"),
+		WithMetricExporterTemporalityPreference("stateless"),
 		WithLogLevel("info"),
 		WithLogger(&suite.testLogger),
 		WithErrorHandler(&suite.testErrorHandler),
 		WithPropagators([]string{"b3"}),
+		WithLightstepMetricsSDK(false),
 	)
 
 	attributes := []attribute.KeyValue{
@@ -451,20 +456,21 @@ func (suite *testSuite) TestConfigurationOverrides() {
 	}
 
 	expected := Config{
-		ServiceName:                    "override-service-name",
-		ServiceVersion:                 "override-service-version",
-		SpanExporterEndpoint:           "override-satellite-url",
-		SpanExporterEndpointInsecure:   false,
-		MetricExporterEndpoint:         "override-metrics-url",
-		MetricExporterEndpointInsecure: false,
-		MetricReportingPeriod:          "30s",
-		MetricTemporalityPreference:    "stateless",
-		Headers:                        map[string]string{"lightstep-access-token": "override-access-token"},
-		LogLevel:                       "info",
-		Propagators:                    []string{"b3"},
-		Resource:                       resource.NewWithAttributes(semconv.SchemaURL, attributes...),
-		logger:                         &suite.testLogger,
-		errorHandler:                   &suite.testErrorHandler,
+		ServiceName:                         "override-service-name",
+		ServiceVersion:                      "override-service-version",
+		SpanExporterEndpoint:                "override-satellite-url",
+		SpanExporterEndpointInsecure:        false,
+		MetricExporterEndpoint:              "override-metrics-url",
+		MetricExporterEndpointInsecure:      false,
+		MetricReportingPeriod:               "30s",
+		MetricExporterTemporalityPreference: "stateless",
+		Headers:                             map[string]string{"lightstep-access-token": "override-access-token"},
+		LogLevel:                            "info",
+		Propagators:                         []string{"b3"},
+		Resource:                            resource.NewWithAttributes(semconv.SchemaURL, attributes...),
+		UseLightstepMetricsSDK:              false,
+		logger:                              &suite.testLogger,
+		errorHandler:                        &suite.testErrorHandler,
 	}
 	assert.Equal(expected, config)
 }
@@ -670,6 +676,7 @@ func setEnvironment() {
 	os.Setenv("OTEL_RESOURCE_ATTRIBUTES", "service.name=test-service-name-b")
 	os.Setenv("OTEL_EXPORTER_OTLP_METRIC_TEMPORALITY_PREFERENCE", "delta")
 	os.Setenv("LS_METRICS_ENABLED", "false")
+	os.Setenv("LS_METRICS_SDK", "true")
 }
 
 func unsetEnvironment() {
@@ -687,6 +694,7 @@ func unsetEnvironment() {
 		"OTEL_EXPORTER_OTLP_METRIC_PERIOD",
 		"OTEL_EXPORTER_OTLP_METRIC_TEMPORALITY_PREFERENCE",
 		"LS_METRICS_ENABLED",
+		"LS_METRICS_SDK",
 	}
 	for _, envvar := range vars {
 		os.Unsetenv(envvar)
@@ -697,3 +705,19 @@ func TestMain(m *testing.M) {
 	unsetEnvironment()
 	os.Exit(m.Run())
 }
+
+func (suite *testSuite) TestLightstepMetricsSDK() {
+	lsOtel := ConfigureOpentelemetry(
+		append(suite.bothInsecureEndpointOptions(),
+			WithServiceName("test-service"),
+			WithAccessToken(fakeAccessToken()),
+			WithLightstepMetricsSDK(true),
+		)...,
+	)
+	defer lsOtel.Shutdown()
+
+	sdk := metricglobal.MeterProvider()
+	if _, ok := sdk.(*sdkmetric.MeterProvider); !ok {
+		suite.T().Errorf("did not find a lightstep metrics SDK")
+	}
+}
diff --git a/lightstep/sdk/metric/internal/asyncstate/async_test.go b/lightstep/sdk/metric/internal/asyncstate/async_test.go
index 248b69e..1b20c83 100644
--- a/lightstep/sdk/metric/internal/asyncstate/async_test.go
+++ b/lightstep/sdk/metric/internal/asyncstate/async_test.go
@@ -16,7 +16,9 @@ package asyncstate // import "github.com/lightstep/otel-launcher-go/lightstep/sd
 
 import (
 	"context"
+	"errors"
 	"math"
+	"strings"
 	"testing"
 	"time"
 
@@ -347,13 +349,13 @@ func TestCallbackDisabledInstrument(t *testing.T) {
 }
 
 func TestOutOfRangeValues(t *testing.T) {
-	errors := test.OTelErrors()
+	otelErrs := test.OTelErrors()
 
 	tt := testAsync("test")
 
-	c := testObserver[float64, number.Float64Traits](tt, "c", sdkinstrument.AsyncCounter)
-	u := testObserver[float64, number.Float64Traits](tt, "u", sdkinstrument.AsyncUpDownCounter)
-	g := testObserver[float64, number.Float64Traits](tt, "g", sdkinstrument.AsyncGauge)
+	c := testObserver[float64, number.Float64Traits](tt, "testPatternC", sdkinstrument.AsyncCounter)
+	u := testObserver[float64, number.Float64Traits](tt, "testPatternU", sdkinstrument.AsyncUpDownCounter)
+	g := testObserver[float64, number.Float64Traits](tt, "testPatternG", sdkinstrument.AsyncGauge)
 
 	cb, _ := NewCallback([]instrument.Asynchronous{
 		c, u, g,
@@ -402,7 +404,14 @@ func TestOutOfRangeValues(t *testing.T) {
 	}
 
 	// 2 readers x 3 error conditions x 3 instruments
-	require.Equal(t, 2*3*3, len(*errors))
-	require.Contains(t, (*errors), aggregator.ErrNaNInput)
-	require.Contains(t, (*errors), aggregator.ErrInfInput)
+	require.Equal(t, 2*3*3, len(*otelErrs))
+
+	for _, err := range *otelErrs {
+		isNaN := errors.Is(err, aggregator.ErrNaNInput)
+		isInf := errors.Is(err, aggregator.ErrInfInput)
+		isNeg := errors.Is(err, aggregator.ErrNegativeInput)
+
+		require.True(t, isNaN || isInf || isNeg)
+		require.True(t, strings.HasPrefix(err.Error(), "testPattern"))
+	}
 }