@@ -8,20 +8,30 @@ import (
88 "os"
99 "strconv"
1010 "strings"
11+ "sync"
1112 "time"
1213
1314 "github.com/spf13/cobra"
15+ "go.opentelemetry.io/otel/trace"
1416 "golang.org/x/xerrors"
1517
1618 "github.com/coder/coder/cli/cliflag"
19+ "github.com/coder/coder/coderd/tracing"
1720 "github.com/coder/coder/codersdk"
1821 "github.com/coder/coder/loadtest/harness"
1922)
2023
24+ const loadtestTracerName = "coder_loadtest"
25+
2126func loadtest () * cobra.Command {
2227 var (
2328 configPath string
2429 outputSpecs []string
30+
31+ traceEnable bool
32+ traceCoder bool
33+ traceHoneycombAPIKey string
34+ tracePropagate bool
2535 )
2636 cmd := & cobra.Command {
2737 Use : "loadtest --config <path> [--output json[:path]] [--output text[:path]]]" ,
@@ -53,6 +63,8 @@ func loadtest() *cobra.Command {
5363 Hidden : true ,
5464 Args : cobra .ExactArgs (0 ),
5565 RunE : func (cmd * cobra.Command , args []string ) error {
66+ ctx := tracing .SetTracerName (cmd .Context (), loadtestTracerName )
67+
5668 config , err := loadLoadTestConfigFile (configPath , cmd .InOrStdin ())
5769 if err != nil {
5870 return err
@@ -67,7 +79,7 @@ func loadtest() *cobra.Command {
6779 return err
6880 }
6981
70- me , err := client .User (cmd . Context () , codersdk .Me )
82+ me , err := client .User (ctx , codersdk .Me )
7183 if err != nil {
7284 return xerrors .Errorf ("fetch current user: %w" , err )
7385 }
@@ -84,11 +96,43 @@ func loadtest() *cobra.Command {
8496 }
8597 }
8698 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+ }()
88129 }
130+ tracer := tracerProvider .Tracer (loadtestTracerName )
89131
90- // Disable ratelimits for future requests.
132+ // Disable ratelimits and propagate tracing spans for future
133+ // requests. Individual tests will setup their own loggers.
91134 client .BypassRatelimits = true
135+ client .PropagateTracing = tracePropagate
92136
93137 // Prepare the test.
94138 strategy := config .Strategy .ExecutionStrategy ()
@@ -99,18 +143,22 @@ func loadtest() *cobra.Command {
99143
100144 for j := 0 ; j < t .Count ; j ++ {
101145 id := strconv .Itoa (j )
102- runner , err := t .NewRunner (client )
146+ runner , err := t .NewRunner (client . Clone () )
103147 if err != nil {
104148 return xerrors .Errorf ("create %q runner for %s/%s: %w" , t .Type , name , id , err )
105149 }
106150
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+ })
108156 }
109157 }
110158
111159 _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "Running load test..." )
112160
113- testCtx := cmd . Context ()
161+ testCtx := ctx
114162 if config .Timeout > 0 {
115163 var cancel func ()
116164 testCtx , cancel = context .WithTimeout (testCtx , time .Duration (config .Timeout ))
@@ -158,11 +206,24 @@ func loadtest() *cobra.Command {
158206
159207 // Cleanup.
160208 _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "\n Cleaning up..." )
161- err = th .Cleanup (cmd . Context () )
209+ err = th .Cleanup (ctx )
162210 if err != nil {
163211 return xerrors .Errorf ("cleanup tests: %w" , err )
164212 }
165213
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+
166227 if res .TotalFail > 0 {
167228 return xerrors .New ("load test failed, see above for more details" )
168229 }
@@ -173,6 +234,12 @@ func loadtest() *cobra.Command {
173234
174235 cliflag .StringVarP (cmd .Flags (), & configPath , "config" , "" , "CODER_LOADTEST_CONFIG_PATH" , "" , "Path to the load test configuration file, or - to read from stdin." )
175236 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+
176243 return cmd
177244}
178245
@@ -271,3 +338,53 @@ func parseLoadTestOutputs(outputs []string) ([]loadTestOutput, error) {
271338
272339 return out , nil
273340}
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