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 stdouttrace_test
16
17import (
18	"bytes"
19	"context"
20	"encoding/json"
21	"testing"
22	"time"
23
24	"github.com/stretchr/testify/assert"
25	"github.com/stretchr/testify/require"
26
27	"go.opentelemetry.io/otel/attribute"
28	"go.opentelemetry.io/otel/codes"
29	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
30	"go.opentelemetry.io/otel/sdk/resource"
31	tracesdk "go.opentelemetry.io/otel/sdk/trace"
32	"go.opentelemetry.io/otel/sdk/trace/tracetest"
33	"go.opentelemetry.io/otel/trace"
34)
35
36func TestExporter_ExportSpan(t *testing.T) {
37	// setup test span
38	now := time.Now()
39	traceID, _ := trace.TraceIDFromHex("0102030405060708090a0b0c0d0e0f10")
40	spanID, _ := trace.SpanIDFromHex("0102030405060708")
41	traceState, _ := trace.ParseTraceState("key=val")
42	keyValue := "value"
43	doubleValue := 123.456
44	resource := resource.NewSchemaless(attribute.String("rk1", "rv11"))
45
46	ss := tracetest.SpanStub{
47		SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
48			TraceID:    traceID,
49			SpanID:     spanID,
50			TraceState: traceState,
51		}),
52		Name:      "/foo",
53		StartTime: now,
54		EndTime:   now,
55		Attributes: []attribute.KeyValue{
56			attribute.String("key", keyValue),
57			attribute.Float64("double", doubleValue),
58		},
59		Events: []tracesdk.Event{
60			{Name: "foo", Attributes: []attribute.KeyValue{attribute.String("key", keyValue)}, Time: now},
61			{Name: "bar", Attributes: []attribute.KeyValue{attribute.Float64("double", doubleValue)}, Time: now},
62		},
63		SpanKind: trace.SpanKindInternal,
64		Status: tracesdk.Status{
65			Code:        codes.Error,
66			Description: "interesting",
67		},
68		Resource: resource,
69	}
70
71	tests := []struct {
72		opts      []stdouttrace.Option
73		expectNow time.Time
74	}{
75		{
76			opts:      []stdouttrace.Option{stdouttrace.WithPrettyPrint()},
77			expectNow: now,
78		},
79		{
80			opts: []stdouttrace.Option{stdouttrace.WithPrettyPrint(), stdouttrace.WithoutTimestamps()},
81			// expectNow is an empty time.Time
82		},
83	}
84
85	ctx := context.Background()
86	for _, tt := range tests {
87		// write to buffer for testing
88		var b bytes.Buffer
89		ex, err := stdouttrace.New(append(tt.opts, stdouttrace.WithWriter(&b))...)
90		require.Nil(t, err)
91
92		err = ex.ExportSpans(ctx, tracetest.SpanStubs{ss, ss}.Snapshots())
93		require.Nil(t, err)
94
95		got := b.String()
96		wantone := expectedJSON(tt.expectNow)
97		assert.Equal(t, wantone+wantone, got)
98	}
99}
100
101func expectedJSON(now time.Time) string {
102	serializedNow, _ := json.Marshal(now)
103	return `{
104	"Name": "/foo",
105	"SpanContext": {
106		"TraceID": "0102030405060708090a0b0c0d0e0f10",
107		"SpanID": "0102030405060708",
108		"TraceFlags": "00",
109		"TraceState": "key=val",
110		"Remote": false
111	},
112	"Parent": {
113		"TraceID": "00000000000000000000000000000000",
114		"SpanID": "0000000000000000",
115		"TraceFlags": "00",
116		"TraceState": "",
117		"Remote": false
118	},
119	"SpanKind": 1,
120	"StartTime": ` + string(serializedNow) + `,
121	"EndTime": ` + string(serializedNow) + `,
122	"Attributes": [
123		{
124			"Key": "key",
125			"Value": {
126				"Type": "STRING",
127				"Value": "value"
128			}
129		},
130		{
131			"Key": "double",
132			"Value": {
133				"Type": "FLOAT64",
134				"Value": 123.456
135			}
136		}
137	],
138	"Events": [
139		{
140			"Name": "foo",
141			"Attributes": [
142				{
143					"Key": "key",
144					"Value": {
145						"Type": "STRING",
146						"Value": "value"
147					}
148				}
149			],
150			"DroppedAttributeCount": 0,
151			"Time": ` + string(serializedNow) + `
152		},
153		{
154			"Name": "bar",
155			"Attributes": [
156				{
157					"Key": "double",
158					"Value": {
159						"Type": "FLOAT64",
160						"Value": 123.456
161					}
162				}
163			],
164			"DroppedAttributeCount": 0,
165			"Time": ` + string(serializedNow) + `
166		}
167	],
168	"Links": null,
169	"Status": {
170		"Code": "Error",
171		"Description": "interesting"
172	},
173	"DroppedAttributes": 0,
174	"DroppedEvents": 0,
175	"DroppedLinks": 0,
176	"ChildSpanCount": 0,
177	"Resource": [
178		{
179			"Key": "rk1",
180			"Value": {
181				"Type": "STRING",
182				"Value": "rv11"
183			}
184		}
185	],
186	"InstrumentationLibrary": {
187		"Name": "",
188		"Version": "",
189		"SchemaURL": ""
190	}
191}
192`
193}
194
195func TestExporterShutdownHonorsTimeout(t *testing.T) {
196	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
197	defer cancel()
198
199	e, err := stdouttrace.New()
200	if err != nil {
201		t.Fatalf("failed to create exporter: %v", err)
202	}
203
204	innerCtx, innerCancel := context.WithTimeout(ctx, time.Nanosecond)
205	defer innerCancel()
206	<-innerCtx.Done()
207	err = e.Shutdown(innerCtx)
208	assert.ErrorIs(t, err, context.DeadlineExceeded)
209}
210
211func TestExporterShutdownHonorsCancel(t *testing.T) {
212	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
213	defer cancel()
214
215	e, err := stdouttrace.New()
216	if err != nil {
217		t.Fatalf("failed to create exporter: %v", err)
218	}
219
220	innerCtx, innerCancel := context.WithCancel(ctx)
221	innerCancel()
222	err = e.Shutdown(innerCtx)
223	assert.ErrorIs(t, err, context.Canceled)
224}
225
226func TestExporterShutdownNoError(t *testing.T) {
227	e, err := stdouttrace.New()
228	if err != nil {
229		t.Fatalf("failed to create exporter: %v", err)
230	}
231
232	if err := e.Shutdown(context.Background()); err != nil {
233		t.Errorf("shutdown errored: expected nil, got %v", err)
234	}
235}
236