@@ -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 {
2227var (
2328configPath string
2429outputSpecs []string
30+
31+ traceEnable bool
32+ traceCoder bool
33+ traceHoneycombAPIKey string
34+ tracePropagate bool
2535)
2636cmd := & cobra.Command {
2737Use :"loadtest --config <path> [--output json[:path]] [--output text[:path]]]" ,
@@ -53,6 +63,8 @@ func loadtest() *cobra.Command {
5363Hidden :true ,
5464Args :cobra .ExactArgs (0 ),
5565RunE :func (cmd * cobra.Command ,args []string )error {
66+ ctx := tracing .SetTracerName (cmd .Context (),loadtestTracerName )
67+
5668config ,err := loadLoadTestConfigFile (configPath ,cmd .InOrStdin ())
5769if err != nil {
5870return err
@@ -67,7 +79,7 @@ func loadtest() *cobra.Command {
6779return err
6880}
6981
70- me ,err := client .User (cmd . Context () ,codersdk .Me )
82+ me ,err := client .User (ctx ,codersdk .Me )
7183if err != nil {
7284return xerrors .Errorf ("fetch current user: %w" ,err )
7385}
@@ -84,11 +96,43 @@ func loadtest() *cobra.Command {
8496}
8597}
8698if ! 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.
91134client .BypassRatelimits = true
135+ client .PropagateTracing = tracePropagate
92136
93137// Prepare the test.
94138strategy := config .Strategy .ExecutionStrategy ()
@@ -99,18 +143,22 @@ func loadtest() *cobra.Command {
99143
100144for j := 0 ;j < t .Count ;j ++ {
101145id := strconv .Itoa (j )
102- runner ,err := t .NewRunner (client )
146+ runner ,err := t .NewRunner (client . Clone () )
103147if err != nil {
104148return 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
114162if config .Timeout > 0 {
115163var cancel func ()
116164testCtx ,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 )
162210if err != nil {
163211return 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+
166227if res .TotalFail > 0 {
167228return xerrors .New ("load test failed, see above for more details" )
168229}
@@ -173,6 +234,12 @@ func loadtest() *cobra.Command {
173234
174235cliflag .StringVarP (cmd .Flags (),& configPath ,"config" ,"" ,"CODER_LOADTEST_CONFIG_PATH" ,"" ,"Path to the load test configuration file, or - to read from stdin." )
175236cliflag .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+
176243return cmd
177244}
178245
@@ -271,3 +338,53 @@ func parseLoadTestOutputs(outputs []string) ([]loadTestOutput, error) {
271338
272339return 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+ }