1// Copyright The OpenTelemetry Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package stdoutmetric_test
16
17import (
18	"bytes"
19	"context"
20	"encoding/json"
21	"fmt"
22	"strings"
23	"testing"
24	"time"
25
26	"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
27
28	"github.com/stretchr/testify/assert"
29	"github.com/stretchr/testify/require"
30
31	"go.opentelemetry.io/otel/attribute"
32	"go.opentelemetry.io/otel/metric"
33	"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
34	controller "go.opentelemetry.io/otel/sdk/metric/controller/basic"
35	processor "go.opentelemetry.io/otel/sdk/metric/processor/basic"
36	"go.opentelemetry.io/otel/sdk/metric/processor/processortest"
37	"go.opentelemetry.io/otel/sdk/resource"
38)
39
40type testFixture struct {
41	t        *testing.T
42	ctx      context.Context
43	cont     *controller.Controller
44	meter    metric.Meter
45	exporter *stdoutmetric.Exporter
46	output   *bytes.Buffer
47}
48
49var testResource = resource.NewSchemaless(attribute.String("R", "V"))
50
51func newFixture(t *testing.T, opts ...stdoutmetric.Option) testFixture {
52	return newFixtureWithResource(t, testResource, opts...)
53}
54
55func newFixtureWithResource(t *testing.T, res *resource.Resource, opts ...stdoutmetric.Option) testFixture {
56	buf := &bytes.Buffer{}
57	opts = append(opts, stdoutmetric.WithWriter(buf))
58	opts = append(opts, stdoutmetric.WithoutTimestamps())
59	exp, err := stdoutmetric.New(opts...)
60	if err != nil {
61		t.Fatal("Error building fixture: ", err)
62	}
63	aggSel := processortest.AggregatorSelector()
64	proc := processor.NewFactory(aggSel, aggregation.StatelessTemporalitySelector())
65	cont := controller.New(proc,
66		controller.WithExporter(exp),
67		controller.WithResource(res),
68	)
69	ctx := context.Background()
70	require.NoError(t, cont.Start(ctx))
71	meter := cont.Meter("test")
72
73	return testFixture{
74		t:        t,
75		ctx:      ctx,
76		exporter: exp,
77		cont:     cont,
78		meter:    meter,
79		output:   buf,
80	}
81}
82
83func (fix testFixture) Output() string {
84	return strings.TrimSpace(fix.output.String())
85}
86
87func TestStdoutTimestamp(t *testing.T) {
88	var buf bytes.Buffer
89	aggSel := processortest.AggregatorSelector()
90	proc := processor.NewFactory(aggSel, aggregation.CumulativeTemporalitySelector())
91	exporter, err := stdoutmetric.New(
92		stdoutmetric.WithWriter(&buf),
93	)
94	if err != nil {
95		t.Fatal("Invalid config: ", err)
96	}
97	cont := controller.New(proc,
98		controller.WithExporter(exporter),
99		controller.WithResource(testResource),
100	)
101	ctx := context.Background()
102
103	require.NoError(t, cont.Start(ctx))
104	meter := cont.Meter("test")
105	counter := metric.Must(meter).NewInt64Counter("name.lastvalue")
106
107	before := time.Now()
108	// Ensure the timestamp is after before.
109	time.Sleep(time.Nanosecond)
110
111	counter.Add(ctx, 1)
112
113	require.NoError(t, cont.Stop(ctx))
114
115	// Ensure the timestamp is before after.
116	time.Sleep(time.Nanosecond)
117	after := time.Now()
118
119	var printed []interface{}
120	if err := json.Unmarshal(buf.Bytes(), &printed); err != nil {
121		t.Fatal("JSON parse error: ", err)
122	}
123
124	require.Len(t, printed, 1)
125	lastValue, ok := printed[0].(map[string]interface{})
126	require.True(t, ok, "last value format")
127	require.Contains(t, lastValue, "Timestamp")
128	lastValueTS := lastValue["Timestamp"].(string)
129	lastValueTimestamp, err := time.Parse(time.RFC3339Nano, lastValueTS)
130	if err != nil {
131		t.Fatal("JSON parse error: ", lastValueTS, ": ", err)
132	}
133
134	assert.True(t, lastValueTimestamp.After(before))
135	assert.True(t, lastValueTimestamp.Before(after))
136}
137
138func TestStdoutCounterFormat(t *testing.T) {
139	fix := newFixture(t)
140
141	counter := metric.Must(fix.meter).NewInt64Counter("name.sum")
142	counter.Add(fix.ctx, 123, attribute.String("A", "B"), attribute.String("C", "D"))
143
144	require.NoError(t, fix.cont.Stop(fix.ctx))
145
146	require.Equal(t, `[{"Name":"name.sum{R=V,instrumentation.name=test,A=B,C=D}","Sum":123}]`, fix.Output())
147}
148
149func TestStdoutLastValueFormat(t *testing.T) {
150	fix := newFixture(t)
151
152	counter := metric.Must(fix.meter).NewFloat64Counter("name.lastvalue")
153	counter.Add(fix.ctx, 123.456, attribute.String("A", "B"), attribute.String("C", "D"))
154
155	require.NoError(t, fix.cont.Stop(fix.ctx))
156
157	require.Equal(t, `[{"Name":"name.lastvalue{R=V,instrumentation.name=test,A=B,C=D}","Last":123.456}]`, fix.Output())
158}
159
160func TestStdoutMinMaxSumCount(t *testing.T) {
161	fix := newFixture(t)
162
163	counter := metric.Must(fix.meter).NewFloat64Counter("name.minmaxsumcount")
164	counter.Add(fix.ctx, 123.456, attribute.String("A", "B"), attribute.String("C", "D"))
165	counter.Add(fix.ctx, 876.543, attribute.String("A", "B"), attribute.String("C", "D"))
166
167	require.NoError(t, fix.cont.Stop(fix.ctx))
168
169	require.Equal(t, `[{"Name":"name.minmaxsumcount{R=V,instrumentation.name=test,A=B,C=D}","Min":123.456,"Max":876.543,"Sum":999.999,"Count":2}]`, fix.Output())
170}
171
172func TestStdoutHistogramFormat(t *testing.T) {
173	fix := newFixture(t, stdoutmetric.WithPrettyPrint())
174
175	inst := metric.Must(fix.meter).NewFloat64Histogram("name.histogram")
176
177	for i := 0; i < 1000; i++ {
178		inst.Record(fix.ctx, float64(i)+0.5, attribute.String("A", "B"), attribute.String("C", "D"))
179	}
180	require.NoError(t, fix.cont.Stop(fix.ctx))
181
182	// TODO: Stdout does not export `Count` for histogram, nor the buckets.
183	require.Equal(t, `[
184	{
185		"Name": "name.histogram{R=V,instrumentation.name=test,A=B,C=D}",
186		"Sum": 500000
187	}
188]`, fix.Output())
189}
190
191func TestStdoutNoData(t *testing.T) {
192	runTwoAggs := func(aggName string) {
193		t.Run(aggName, func(t *testing.T) {
194			t.Parallel()
195
196			fix := newFixture(t)
197			_ = metric.Must(fix.meter).NewFloat64Counter(fmt.Sprint("name.", aggName))
198			require.NoError(t, fix.cont.Stop(fix.ctx))
199
200			require.Equal(t, "", fix.Output())
201		})
202	}
203
204	runTwoAggs("lastvalue")
205	runTwoAggs("minmaxsumcount")
206}
207
208func TestStdoutResource(t *testing.T) {
209	type testCase struct {
210		name   string
211		expect string
212		res    *resource.Resource
213		attrs  []attribute.KeyValue
214	}
215	newCase := func(name, expect string, res *resource.Resource, attrs ...attribute.KeyValue) testCase {
216		return testCase{
217			name:   name,
218			expect: expect,
219			res:    res,
220			attrs:  attrs,
221		}
222	}
223	testCases := []testCase{
224		newCase("resource and attribute",
225			"R1=V1,R2=V2,instrumentation.name=test,A=B,C=D",
226			resource.NewSchemaless(attribute.String("R1", "V1"), attribute.String("R2", "V2")),
227			attribute.String("A", "B"),
228			attribute.String("C", "D")),
229		newCase("only resource",
230			"R1=V1,R2=V2,instrumentation.name=test",
231			resource.NewSchemaless(attribute.String("R1", "V1"), attribute.String("R2", "V2")),
232		),
233		newCase("empty resource",
234			"instrumentation.name=test,A=B,C=D",
235			resource.Empty(),
236			attribute.String("A", "B"),
237			attribute.String("C", "D"),
238		),
239		newCase("default resource",
240			fmt.Sprint(resource.Default().Encoded(attribute.DefaultEncoder()),
241				",instrumentation.name=test,A=B,C=D"),
242			resource.Default(),
243			attribute.String("A", "B"),
244			attribute.String("C", "D"),
245		),
246		// We explicitly do not de-duplicate between resources
247		// and metric labels in this exporter.
248		newCase("resource deduplication",
249			"R1=V1,R2=V2,instrumentation.name=test,R1=V3,R2=V4",
250			resource.NewSchemaless(attribute.String("R1", "V1"), attribute.String("R2", "V2")),
251			attribute.String("R1", "V3"),
252			attribute.String("R2", "V4")),
253	}
254
255	for _, tc := range testCases {
256		t.Run(tc.name, func(t *testing.T) {
257			ctx := context.Background()
258			fix := newFixtureWithResource(t, tc.res)
259
260			counter := metric.Must(fix.meter).NewFloat64Counter("name.lastvalue")
261			counter.Add(ctx, 123.456, tc.attrs...)
262
263			require.NoError(t, fix.cont.Stop(fix.ctx))
264
265			require.Equal(t, `[{"Name":"name.lastvalue{`+tc.expect+`}","Last":123.456}]`, fix.Output())
266		})
267	}
268}
269