@@ -8,20 +8,30 @@ import (
8
8
"os"
9
9
"strconv"
10
10
"strings"
11
+ "sync"
11
12
"time"
12
13
13
14
"github.com/spf13/cobra"
15
+ "go.opentelemetry.io/otel/trace"
14
16
"golang.org/x/xerrors"
15
17
16
18
"github.com/coder/coder/cli/cliflag"
19
+ "github.com/coder/coder/coderd/tracing"
17
20
"github.com/coder/coder/codersdk"
18
21
"github.com/coder/coder/loadtest/harness"
19
22
)
20
23
24
+ const loadtestTracerName = "coder_loadtest"
25
+
21
26
func loadtest () * cobra.Command {
22
27
var (
23
28
configPath string
24
29
outputSpecs []string
30
+
31
+ traceEnable bool
32
+ traceCoder bool
33
+ traceHoneycombAPIKey string
34
+ tracePropagate bool
25
35
)
26
36
cmd := & cobra.Command {
27
37
Use : "loadtest --config <path> [--output json[:path]] [--output text[:path]]]" ,
@@ -53,6 +63,8 @@ func loadtest() *cobra.Command {
53
63
Hidden : true ,
54
64
Args : cobra .ExactArgs (0 ),
55
65
RunE : func (cmd * cobra.Command , args []string ) error {
66
+ ctx := tracing .SetTracerName (cmd .Context (), loadtestTracerName )
67
+
56
68
config , err := loadLoadTestConfigFile (configPath , cmd .InOrStdin ())
57
69
if err != nil {
58
70
return err
@@ -67,7 +79,7 @@ func loadtest() *cobra.Command {
67
79
return err
68
80
}
69
81
70
- me , err := client .User (cmd . Context () , codersdk .Me )
82
+ me , err := client .User (ctx , codersdk .Me )
71
83
if err != nil {
72
84
return xerrors .Errorf ("fetch current user: %w" , err )
73
85
}
@@ -84,11 +96,43 @@ func loadtest() *cobra.Command {
84
96
}
85
97
}
86
98
if ! ok {
87
- return xerrors .Errorf ("Not logged in as site owner. Load testing is only available to site owners." )
99
+ return xerrors .Errorf ("Not logged in as a site owner. Load testing is only available to site owners." )
100
+ }
101
+
102
+ // Setup tracing and start a span.
103
+ var (
104
+ shouldTrace = traceEnable || traceCoder || traceHoneycombAPIKey != ""
105
+ tracerProvider trace.TracerProvider = trace .NewNoopTracerProvider ()
106
+ closeTracingOnce sync.Once
107
+ closeTracing = func (_ context.Context ) error {
108
+ return nil
109
+ }
110
+ )
111
+ if shouldTrace {
112
+ tracerProvider , closeTracing , err = tracing .TracerProvider (ctx , loadtestTracerName , tracing.TracerOpts {
113
+ Default : traceEnable ,
114
+ Coder : traceCoder ,
115
+ Honeycomb : traceHoneycombAPIKey ,
116
+ })
117
+ if err != nil {
118
+ return xerrors .Errorf ("initialize tracing: %w" , err )
119
+ }
120
+ defer func () {
121
+ closeTracingOnce .Do (func () {
122
+ // Allow time for traces to flush even if command
123
+ // context is canceled.
124
+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
125
+ defer cancel ()
126
+ _ = closeTracing (ctx )
127
+ })
128
+ }()
88
129
}
130
+ tracer := tracerProvider .Tracer (loadtestTracerName )
89
131
90
- // Disable ratelimits for future requests.
132
+ // Disable ratelimits and propagate tracing spans for future
133
+ // requests. Individual tests will setup their own loggers.
91
134
client .BypassRatelimits = true
135
+ client .PropagateTracing = tracePropagate
92
136
93
137
// Prepare the test.
94
138
strategy := config .Strategy .ExecutionStrategy ()
@@ -99,18 +143,22 @@ func loadtest() *cobra.Command {
99
143
100
144
for j := 0 ; j < t .Count ; j ++ {
101
145
id := strconv .Itoa (j )
102
- runner , err := t .NewRunner (client )
146
+ runner , err := t .NewRunner (client . Clone () )
103
147
if err != nil {
104
148
return xerrors .Errorf ("create %q runner for %s/%s: %w" , t .Type , name , id , err )
105
149
}
106
150
107
- th .AddRun (name , id , runner )
151
+ th .AddRun (name , id , & runnableTraceWrapper {
152
+ tracer : tracer ,
153
+ spanName : fmt .Sprintf ("%s/%s" , name , id ),
154
+ runner : runner ,
155
+ })
108
156
}
109
157
}
110
158
111
159
_ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "Running load test..." )
112
160
113
- testCtx := cmd . Context ()
161
+ testCtx := ctx
114
162
if config .Timeout > 0 {
115
163
var cancel func ()
116
164
testCtx , cancel = context .WithTimeout (testCtx , time .Duration (config .Timeout ))
@@ -158,11 +206,24 @@ func loadtest() *cobra.Command {
158
206
159
207
// Cleanup.
160
208
_ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "\n Cleaning up..." )
161
- err = th .Cleanup (cmd . Context () )
209
+ err = th .Cleanup (ctx )
162
210
if err != nil {
163
211
return xerrors .Errorf ("cleanup tests: %w" , err )
164
212
}
165
213
214
+ // Upload traces.
215
+ if shouldTrace {
216
+ _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "\n Uploading traces..." )
217
+ closeTracingOnce .Do (func () {
218
+ ctx , cancel := context .WithTimeout (ctx , 1 * time .Minute )
219
+ defer cancel ()
220
+ err := closeTracing (ctx )
221
+ if err != nil {
222
+ _ , _ = fmt .Fprintf (cmd .ErrOrStderr (), "\n Error uploading traces: %+v\n " , err )
223
+ }
224
+ })
225
+ }
226
+
166
227
if res .TotalFail > 0 {
167
228
return xerrors .New ("load test failed, see above for more details" )
168
229
}
@@ -173,6 +234,12 @@ func loadtest() *cobra.Command {
173
234
174
235
cliflag .StringVarP (cmd .Flags (), & configPath , "config" , "" , "CODER_LOADTEST_CONFIG_PATH" , "" , "Path to the load test configuration file, or - to read from stdin." )
175
236
cliflag .StringArrayVarP (cmd .Flags (), & outputSpecs , "output" , "" , "CODER_LOADTEST_OUTPUTS" , []string {"text" }, "Output formats, see usage for more information." )
237
+
238
+ cliflag .BoolVarP (cmd .Flags (), & traceEnable , "trace" , "" , "CODER_LOADTEST_TRACE" , false , "Whether application tracing data is collected. It exports to a backend configured by environment variables. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md" )
239
+ cliflag .BoolVarP (cmd .Flags (), & traceCoder , "trace-coder" , "" , "CODER_LOADTEST_TRACE_CODER" , false , "Whether opentelemetry traces are sent to Coder. We recommend keeping this disabled unless we advise you to enable it." )
240
+ cliflag .StringVarP (cmd .Flags (), & traceHoneycombAPIKey , "trace-honeycomb-api-key" , "" , "CODER_LOADTEST_TRACE_HONEYCOMB_API_KEY" , "" , "Enables trace exporting to Honeycomb.io using the provided API key." )
241
+ cliflag .BoolVarP (cmd .Flags (), & tracePropagate , "trace-propagate" , "" , "CODER_LOADTEST_TRACE_PROPAGATE" , false , "Enables trace propagation to the Coder backend, which will be used to correlate server-side spans with client-side spans. Only enable this if the server is configured with the exact same tracing configuration as the client." )
242
+
176
243
return cmd
177
244
}
178
245
@@ -271,3 +338,53 @@ func parseLoadTestOutputs(outputs []string) ([]loadTestOutput, error) {
271
338
272
339
return out , nil
273
340
}
341
+
342
+ type runnableTraceWrapper struct {
343
+ tracer trace.Tracer
344
+ spanName string
345
+ runner harness.Runnable
346
+
347
+ span trace.Span
348
+ }
349
+
350
+ var _ harness.Runnable = & runnableTraceWrapper {}
351
+ var _ harness.Cleanable = & runnableTraceWrapper {}
352
+
353
+ func (r * runnableTraceWrapper ) Run (ctx context.Context , id string , logs io.Writer ) error {
354
+ ctx , span := r .tracer .Start (ctx , r .spanName , trace .WithNewRoot ())
355
+ defer span .End ()
356
+ r .span = span
357
+
358
+ traceID := "unknown trace ID"
359
+ spanID := "unknown span ID"
360
+ if span .SpanContext ().HasTraceID () {
361
+ traceID = span .SpanContext ().TraceID ().String ()
362
+ }
363
+ if span .SpanContext ().HasSpanID () {
364
+ spanID = span .SpanContext ().SpanID ().String ()
365
+ }
366
+ _ , _ = fmt .Fprintf (logs , "Trace ID: %s\n " , traceID )
367
+ _ , _ = fmt .Fprintf (logs , "Span ID: %s\n \n " , spanID )
368
+
369
+ // Make a separate span for the run itself so the sub-spans are grouped
370
+ // neatly. The cleanup span is also a child of the above span so this is
371
+ // important for readability.
372
+ ctx2 , span2 := r .tracer .Start (ctx , r .spanName + " run" )
373
+ defer span2 .End ()
374
+ return r .runner .Run (ctx2 , id , logs )
375
+ }
376
+
377
+ func (r * runnableTraceWrapper ) Cleanup (ctx context.Context , id string ) error {
378
+ c , ok := r .runner .(harness.Cleanable )
379
+ if ! ok {
380
+ return nil
381
+ }
382
+
383
+ if r .span != nil {
384
+ ctx = trace .ContextWithSpanContext (ctx , r .span .SpanContext ())
385
+ }
386
+ ctx , span := r .tracer .Start (ctx , r .spanName + " cleanup" )
387
+ defer span .End ()
388
+
389
+ return c .Cleanup (ctx , id )
390
+ }
0 commit comments