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