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