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

Issue:
I cannot Get Max/Min Value In Histogram type
### Description

I noticed that In DefaultAggregationSelector, the max/min value are recorded by default.  But when I tried to use it with a simple stdout exporter, it comes that min&max are always empty. How Should I use it? is there any example?

![image](https://github.com/open-telemetry/opentelemetry-go/assets/29519679/874eb8df-47da-40ac-be76-9221e977bab0)



![image](https://github.com/open-telemetry/opentelemetry-go/assets/29519679/6af71a5a-fc0f-4917-82d3-95fc99dd7f09)


```go 
func DefaultAggregationSelector(ik InstrumentKind) Aggregation {
	switch ik {
	case InstrumentKindCounter, InstrumentKindUpDownCounter, InstrumentKindObservableCounter, InstrumentKindObservableUpDownCounter:
		return AggregationSum{}
	case InstrumentKindObservableGauge:
		return AggregationLastValue{}
	case InstrumentKindHistogram:
		return AggregationExplicitBucketHistogram{
			Boundaries: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000},
			NoMinMax:   false,
		}
	}
	panic("unknown instrument kind")
}
```



### Environment

- OS: macOS Big Sur (11.x)
- Architecture: arm64
- Go Version: 1.20
- opentelemetry-go version: 1.21.0

### Steps To Reproduce

```go
package main

import (
	"context"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
	otelmetric "go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
	"log"
	"math/rand"
	"net/http"
	"time"
)

func MockOpenTelemetry() {
	// Create resource.
	res, err := newResource()
	if err != nil {
		panic(err)
	}
	meterProvider, err := newMeterProvider(res)
	if err != nil {
		panic(err)
	}

	defer func() {
		if err := meterProvider.Shutdown(context.Background()); err != nil {
			log.Println(err)
		}
	}()

	otel.SetMeterProvider(meterProvider)

	var meter = otel.Meter("my-service-meter")
	histogram, err := meter.Int64Histogram(
		"task.duration",
		otelmetric.WithDescription("The duration of task execution."),
		otelmetric.WithUnit("ms"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// do some work in an API call
		time.Sleep(100*time.Millisecond + time.Duration(rand.Intn(901))*time.Millisecond)

		duration := time.Since(start)
		histogram.Record(r.Context(), duration.Milliseconds())
	})
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func newResource() (*resource.Resource, error) {
	return resource.Merge(resource.Default(),
		resource.NewWithAttributes(semconv.SchemaURL,
			semconv.ServiceName("my-service"),
			semconv.ServiceVersion("0.1.0"),
		))
}

func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
	metricExporter, err := stdoutmetric.New(stdoutmetric.WithTemporalitySelector(deltaSelector), stdoutmetric.WithAggregationSelector(metric.DefaultAggregationSelector))
	if err != nil {
		return nil, err
	}

	meterProvider := metric.NewMeterProvider(
		metric.WithResource(res),
		metric.WithReader(metric.NewPeriodicReader(metricExporter,
			// Default is 1m. Set to 3s for demonstrative purposes.
			metric.WithInterval(1*time.Minute))),
	)
	return meterProvider, nil
}

```

{"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"my-service"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"0.1.0"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.21.0"}}],"ScopeMetrics":[{"Scope":{"Name":"my-service-meter","Version":"","SchemaURL":""},"Metrics":[{"Name":"task.duration","Description":"The duration of task execution.","Unit":"ms","Data":{"DataPoints":[{"Attributes":[],"StartTime":"2024-01-14T13:08:48.504319+08:00","Time":"2024-01-14T13:09:48.50817+08:00","Count":12,"Bounds":[0,5,10,25,50,75,100,250,500,750,1000,2500,5000,7500,10000],"BucketCounts":[0,0,0,0,0,0,0,3,1,3,5,0,0,0,0,0],"Min":{},"Max":{},"Sum":6930}],"Temporality":"DeltaTemporality"}}]}]}
{"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"my-service"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"0.1.0"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.21.0"}}],"ScopeMetrics":[{"Scope":{"Name":"my-service-meter","Version":"","SchemaURL":""},"Metrics":[{"Name":"task.duration","Description":"The duration of task execution.","Unit":"ms","Data":{"DataPoints":[{"Attributes":[],"StartTime":"2024-01-14T13:09:48.50817+08:00","Time":"2024-01-14T13:10:48.506979+08:00","Count":4,"Bounds":[0,5,10,25,50,75,100,250,500,750,1000,2500,5000,7500,10000],"BucketCounts":[0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0],"Min":{},"Max":{},"Sum":1452}],"Temporality":"DeltaTemporality"}}]}]}


### Expected behavior

I expecte to get max/min value in histogram with delta temporality


Discussion: Thank you for the complete bug report. The default JSON encoder of the STDOUT exporter will not encode private fields: https://pkg.go.dev/encoding/json#Encoder.Encode. Running your example with a debugger showed the min/max are being captured, just not displayed.

I think adding `MarshalText` methods to the `Extrema` types will help resolve this.
Hello, I'd like to work on that one



Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1437444568a..8b1b82d8296 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
 
 - Fix `ContainerID` resource detection on systemd when cgroup path has a colon. (#4449)
 - Fix `go.opentelemetry.io/otel/sdk/metric` to cache instruments to avoid leaking memory when the same instrument is created multiple times. (#4820)
+- Fix missing Mix and Max values for `go.opentelemetry.io/otel/exporters/stdout/stdoutmetric` by introducing `MarshalText` and `MarshalJSON` for type `Extrema` in `go.opentelemetry.io/sdk/metric/metricdata`. (#4827)
 
 ## [1.23.0-rc.1] 2024-01-18
 
diff --git a/sdk/metric/metricdata/data.go b/sdk/metric/metricdata/data.go
index 995d42b38f1..32c17934fc4 100644
--- a/sdk/metric/metricdata/data.go
+++ b/sdk/metric/metricdata/data.go
@@ -15,6 +15,7 @@
 package metricdata // import "go.opentelemetry.io/otel/sdk/metric/metricdata"
 
 import (
+	"encoding/json"
 	"time"
 
 	"go.opentelemetry.io/otel/attribute"
@@ -211,6 +212,19 @@ type Extrema[N int64 | float64] struct {
 	valid bool
 }
 
+// MarshalText converts the Extrema value to text.
+func (e Extrema[N]) MarshalText() ([]byte, error) {
+	if !e.valid {
+		return json.Marshal(nil)
+	}
+	return json.Marshal(e.value)
+}
+
+// MarshalJSON converts the Extrema value to JSON number.
+func (e *Extrema[N]) MarshalJSON() ([]byte, error) {
+	return e.MarshalText()
+}
+
 // NewExtrema returns an Extrema set to v.
 func NewExtrema[N int64 | float64](v N) Extrema[N] {
 	return Extrema[N]{value: v, valid: true}