Repository: open-telemetry/opentelemetry-go (Go)

Issue:
Trace SDK observability - otlptracehttp exporter metrics
Add the following [OTel Observability](https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/otel/sdk-metrics.md) for [`otlptracehttp`](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp):

- `otel.sdk.exporter.span.inflight`
- `otel.sdk.exporter.span.exported`
- `otel.sdk.exporter.operation.duration`

## Observability Implementation Checklist

Based on the [project Observability guidelines], ensure the following are completed:

[project Observability guidelines]: https://github.com/open-telemetry/opentelemetry-go/blob/e4ab3141123d0811125a69823dbbe4d9ec5a9b8f/CONTRIBUTING.md#observability

### Environment Variable Activation

- [ ] Observability features are disabled by default
- [ ] Features are activated through the `OTEL_GO_X_OBSERVABILITY` environment variable
- [ ] Use consistent pattern with `x.Observability.Enabled()` check[^1]
- [ ] Follow established experimental feature pattern[^2][^3]

[^1]: <https://github.com/open-telemetry/opentelemetry-go/blob/e4ab3141123d0811125a69823dbbe4d9ec5a9b8f/exporters/stdout/stdouttrace/internal/observ/instrumentation.go#L101-L103>
[^2]: <https://github.com/open-telemetry/opentelemetry-go/blob/e4ab3141123d0811125a69823dbbe4d9ec5a9b8f/exporters/stdout/stdouttrace/internal/x/x.go>
[^3]: <https://github.com/open-telemetry/opentelemetry-go/blob/e4ab3141123d0811125a69823dbbe4d9ec5a9b8f/sdk/internal/x/x.go>

### Encapsulation

- [ ] Instrumentation is encapsulated within a dedicated `struct` (e.g., [`Instrumentation`])
- [ ] Instrumentation is not mixed into the instrumented component
- [ ] Instrumentation code is in its own file or package if complex/reused
- [ ] Instrumentation setup doesn't bloat the main component code

[`Instrumentation`]: https://github.com/open-telemetry/opentelemetry-go/blob/e4ab3141123d0811125a69823dbbe4d9ec5a9b8f/exporters/stdout/stdouttrace/internal/observ/instrumentation.go#L86-L94

### Initialization

- [ ] Initialization is only done when observability is enabled
- [ ] Setup is explicit and side-effect free
- [ ] Return errors from initialization when appropriate
- [ ] Use the global Meter provider (e.g., `otel.GetMeterProvider()`)
- [ ] Include proper meter configuration with:
  - [ ] Instrumentation package name is used for the Meter
  - [ ] Instrumentation version (e.g. [`Version`])
  - [ ] Schema URL (e.g. [`SchemaURL`])

[`Version`]: https://github.com/open-telemetry/opentelemetry-go/blob/e4ab3141123d0811125a69823dbbe4d9ec5a9b8f/exporters/stdout/stdouttrace/internal/observ/instrumentation.go#L40-L43
[`SchemaURL`]: https://github.com/open-telemetry/opentelemetry-go/blob/e4ab3141123d0811125a69823dbbe4d9ec5a9b8f/exporters/stdout/stdouttrace/internal/observ/instrumentation.go#L36-L38

### Performance

- [ ] Little to no overhead when observability is disabled
- [ ] Expensive operations are only executed when observability is enabled
- [ ] When enabled, instrumentation code paths are optimized to reduce allocation/computation overhead

#### Attribute and Option Allocation Management

- [ ] Use `sync.Pool` for attribute slices and options with dynamic attributes
- [ ] Pool objects are properly reset before returning to pool
- [ ] Pools are scoped for maximum efficiency while ensuring correctness

#### Caching

- [ ] Static attribute sets known at compile time are pre-computed and cached
- [ ] Common attribute combinations use lookup tables/maps

#### Benchmarking

- [ ] Benchmarks provided for all instrumentation code
- [ ] Benchmark scenarios include both enabled and disabled observability
- [ ] Benchmark results show impact on allocs/op, B/op, and ns/op (use `b.ReportAllocs()` in benchmarks)

### Error Handling and Robustness

- [ ] Errors are reported back to caller when possible
- [ ] Partial failures are handled gracefully
- [ ] Use partially initialized components when available
- [ ] Return errors to caller instead of only using `otel.Handle()`
- [ ] Use `otel.Handle()` only when component cannot report error to user

### Context Propagation

- [ ] Observability measurements receive the context from the function being measured (don't break context propagation by using `context.Background()`)

### Semantic Conventions Compliance

- [ ] All metrics follow [OpenTelemetry Semantic Conventions]
- [ ] Use the [`otelconv`] convenience package for metric semantic conventions
- [ ] Component names follow semantic conventions
- [ ] Use package path scope type as stable identifier for non-standard components
- [ ] Component names are stable unique identifiers
- [ ] Use global counter for uniqueness if necessary
- [ ] Component ID counter is resettable for deterministic testing

[OpenTelemetry Semantic Conventions]: https://github.com/open-telemetry/semantic-conventions/blob/5ee549b1ce30fe11fcb9b7e3bd35ebfb363f467f/docs/otel/sdk-metrics.md
[`otelconv`]: https://pkg.go.dev/go.opentelemetry.io/otel@v1.38.0/semconv/v1.37.0/otelconv

### Testing

- [ ] Use deterministic testing with isolated state
- [ ] Restore previous state after tests (`t.Cleanup()`)
- [ ] Isolate meter provider for testing
- [ ] Use `t.Setenv()` for environment variable testing
- [ ] Reset component ID counter for deterministic component names
- [ ] Test order doesn't affect results



Discussion: @pellared I'm happy to work on this
@Mojachieee are you still work? 
> [@Mojachieee](https://github.com/Mojachieee) are you still work?

Nope, I haven't got round to this, feel free to unassign me
@pellared @dmathieu please assign this to me!
@zhengkezhou1 I'm going to unassign you from this issue to allow others to start working on this. Please let me know if you are actively working on this issue, or if there is an associated PR I have missed.
Planned work breakdown:

- [x] Create an internal `x` package to handle experimental enablement flag
  - https://github.com/open-telemetry/opentelemetry-go/pull/7476
- [x] Create a local version to reference in instrumentation
  - https://github.com/open-telemetry/opentelemetry-go/pull/7479
- [x] Create an internal `observ` package to encapsulate the instrumentation for the package
  - https://github.com/open-telemetry/opentelemetry-go/pull/7480
- [ ] Integrate the `observ` package instrumentation into the `client` type's `UploadTraces` method
  - https://github.com/open-telemetry/opentelemetry-go/pull/7486



Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3547687b96..9903d131a56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
   Check the `go.opentelemetry.io/otel/exporters/prometheus/internal/x` package documentation for more information. (#7345)
 - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#7353)
 - Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#7459)
+- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#7486)
 
 ### Fixed
 
diff --git a/exporters/otlp/otlptrace/otlptracehttp/client.go b/exporters/otlp/otlptrace/otlptracehttp/client.go
index 7c3273025c7..d0688c2016f 100644
--- a/exporters/otlp/otlptrace/otlptracehttp/client.go
+++ b/exporters/otlp/otlptrace/otlptracehttp/client.go
@@ -24,6 +24,8 @@ import (
 
 	"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
 	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/counter"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/observ"
 	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig"
 	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/retry"
 )
@@ -62,6 +64,9 @@ type client struct {
 	client      *http.Client
 	stopCh      chan struct{}
 	stopOnce    sync.Once
+
+	instID int64
+	inst   *observ.Instrumentation
 }
 
 var _ otlptrace.Client = (*client)(nil)
@@ -99,18 +104,29 @@ func NewClient(opts ...Option) otlptrace.Client {
 		requestFunc: cfg.RetryConfig.RequestFunc(evaluate),
 		stopCh:      stopCh,
 		client:      httpClient,
+		instID:      counter.NextExporterID(),
 	}
 }
 
 // Start does nothing in a HTTP client.
-func (*client) Start(ctx context.Context) error {
+func (c *client) Start(ctx context.Context) error {
+	// Initialize the instrumentation if not already done.
+	//
+	// Initialize here instead of NewClient to allow any errors to be passed
+	// back to the caller and so that any setup of the environment variables to
+	// enable instrumentation can be set via code.
+	var err error
+	if c.inst == nil {
+		c.inst, err = observ.NewInstrumentation(c.instID, c.cfg.Endpoint)
+	}
+
 	// nothing to do
 	select {
 	case <-ctx.Done():
-		return ctx.Err()
+		err = errors.Join(err, ctx.Err())
 	default:
 	}
-	return nil
+	return err
 }
 
 // Stop shuts down the client and interrupt any in-flight request.
@@ -144,6 +160,12 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
 		return err
 	}
 
+	var statusCode int
+	if d.inst != nil {
+		op := d.inst.ExportSpans(ctx, len(protoSpans))
+		defer func() { op.End(uploadErr, statusCode) }()
+	}
+
 	return errors.Join(uploadErr, d.requestFunc(ctx, func(ctx context.Context) error {
 		select {
 		case <-ctx.Done():
@@ -169,7 +191,8 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
 			}()
 		}
 
-		if sc := resp.StatusCode; sc >= 200 && sc <= 299 {
+		statusCode = resp.StatusCode
+		if statusCode >= 200 && statusCode <= 299 {
 			// Success, do not retry.
 			// Read the partial success message, if any.
 			var respData bytes.Buffer
@@ -213,7 +236,7 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
 		}
 		bodyErr := fmt.Errorf("body: %s", respStr)
 
-		switch resp.StatusCode {
+		switch statusCode {
 		case http.StatusTooManyRequests,
 			http.StatusBadGateway,
 			http.StatusServiceUnavailable,
diff --git a/exporters/otlp/otlptrace/otlptracehttp/internal/counter/counter.go b/exporters/otlp/otlptrace/otlptracehttp/internal/counter/counter.go
new file mode 100644
index 00000000000..75e5c0c7ed3
--- /dev/null
+++ b/exporters/otlp/otlptrace/otlptracehttp/internal/counter/counter.go
@@ -0,0 +1,31 @@
+// Code generated by gotmpl. DO NOT MODIFY.
+// source: internal/shared/counter/counter.go.tmpl
+
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+// Package counter provides a simple counter for generating unique IDs.
+//
+// This package is used to generate unique IDs while allowing testing packages
+// to reset the counter.
+package counter // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/counter"
+
+import "sync/atomic"
+
+// exporterN is a global 0-based count of the number of exporters created.
+var exporterN atomic.Int64
+
+// NextExporterID returns the next unique ID for an exporter.
+func NextExporterID() int64 {
+	const inc = 1
+	return exporterN.Add(inc) - inc
+}
+
+// SetExporterID sets the exporter ID counter to v and returns the previous
+// value.
+//
+// This function is useful for testing purposes, allowing you to reset the
+// counter. It should not be used in production code.
+func SetExporterID(v int64) int64 {
+	return exporterN.Swap(v)
+}
diff --git a/exporters/otlp/otlptrace/otlptracehttp/internal/gen.go b/exporters/otlp/otlptrace/otlptracehttp/internal/gen.go
index a96964636d6..ca387432fd5 100644
--- a/exporters/otlp/otlptrace/otlptracehttp/internal/gen.go
+++ b/exporters/otlp/otlptrace/otlptracehttp/internal/gen.go
@@ -25,4 +25,7 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/ot
 //go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlptracetest/otlptest.go.tmpl "--data={}" --out=otlptracetest/otlptest.go
 
 //go:generate gotmpl --body=../../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\" }" --out=x/x.go
+
+//go:generate gotmpl --body=../../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/counter\" }" --out=counter/counter.go
+//go:generate gotmpl --body=../../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go
 //go:generate gotmpl --body=../../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go