Documentation
¶
Overview ¶
Package jotaro is a library for load-testing HTTP servers with scenarios: a flow such as login → list → order, written as plain Go code.
This package is the entry point. It handles scenarios (Scenario / User / Step) and how load is applied (Plan / Run / Rate / Ramp / Stages). The result is returned as a report.Report. Writing it to the console, JSON or HTML, and checking it against thresholds, is done by the report package; raw per-request records are handled by the record package.
sc := jotaro.NewScenario("checkout", func(ctx context.Context, u *jotaro.User) error {
return u.Step("login", func(s *jotaro.Step) error {
return s.Post("/login").JSON(creds).Do().ExpectStatus(200)
})
})
rep, err := jotaro.Run(ctx, jotaro.Plan{Scenario: sc, BaseURL: url, Users: 20, Duration: time.Minute})
rep.Write(os.Stdout, report.FormatText)
Example ¶
A scenario is plain Go code. Run it with a Plan, here against a local test server.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/RIRUa/jotaro"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/login":
fmt.Fprint(w, `{"token":"secret"}`)
default:
w.WriteHeader(http.StatusCreated)
}
}))
defer srv.Close()
checkout := jotaro.NewScenario("checkout", func(ctx context.Context, u *jotaro.User) error {
var login struct {
Token string `json:"token"`
}
if err := u.Step("login", func(s *jotaro.Step) error {
return s.Post("/login").JSON(map[string]string{"user": "alice"}).Do().Decode(&login)
}); err != nil {
return err // return err to stop; ignore it to continue
}
u.Think(10 * time.Millisecond) // waiting time; not counted in latency
return u.Step("order", func(s *jotaro.Step) error {
return s.Post("/orders").Bearer(login.Token).
JSON(map[string]int{"item": 1}).
Do().ExpectStatus(http.StatusCreated)
})
})
rep, err := jotaro.Run(context.Background(), jotaro.Plan{
Scenario: checkout,
BaseURL: srv.URL,
Duration: 200 * time.Millisecond,
Users: 2,
})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("ran:", rep.Requests.Count > 0)
fmt.Println("errors:", len(rep.Requests.Errors))
}
Output: ran: true errors: 0
Index ¶
- func LoadCSV(r io.Reader) ([]map[string]string, error)
- func Run(ctx context.Context, p Plan) (*report.Report, error)
- type Arrival
- type ExpectError
- type FeedMode
- type Feeder
- type Plan
- type Request
- func (r *Request) Basic(user, pass string) *Request
- func (r *Request) Bearer(token string) *Request
- func (r *Request) Body(v any) *Request
- func (r *Request) Do() *Response
- func (r *Request) Form(v url.Values) *Request
- func (r *Request) Header(key, value string) *Request
- func (r *Request) Headers(h map[string]string) *Request
- func (r *Request) JSON(v any) *Request
- func (r *Request) Queries(v url.Values) *Request
- func (r *Request) Query(key, value string) *Request
- type RequestSpec
- type Response
- type Scenario
- type Stage
- type Step
- type User
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LoadCSV ¶
LoadCSV reads a CSV whose first line is the header into rows keyed by column name.
Example ¶
LoadCSV reads rows keyed by the column names of the header line.
package main
import (
"fmt"
"strings"
"github.com/RIRUa/jotaro"
)
func main() {
rows, err := jotaro.LoadCSV(strings.NewReader("user,password\nalice,a1\nbob,b2\n"))
if err != nil {
fmt.Println(err)
return
}
feeder := jotaro.NewFeeder(rows, jotaro.Once)
row, _ := feeder.Next()
fmt.Println(feeder.Len(), row["user"], row["password"])
}
Output: 2 alice a1
func Run ¶
Run applies load according to the Plan, then aggregates and returns the result.
Flow of a run:
- validate the Plan, and clone the default Transport so connections are not shared with other code
- start the health sampler, then run the closed (Users) or open (Arrival) loop until Duration ends
- wait up to Grace for running iterations, then cancel them
- aggregate the collected records into a report, and write the raw data if Plan.Raw is set
When ctx is done, everything stops at once, including running iterations, and the results so far are returned. Report.Elapsed is the real time until all users have stopped.
Types ¶
type Arrival ¶
type Arrival interface {
// contains filtered or unexported methods
}
Arrival is how scenarios arrive in the open model. Arrival times are decided independently of how long a scenario takes.
func Ramp ¶
Ramp changes the arrival rate linearly from from to to over over. The rate is a count per per; after over has passed it stays at to (if to is 0, arrivals stop there). For example, Ramp(0, 50, time.Second, 30*time.Second) raises the rate from 0 to 50 per second over 30 seconds.
The length of the whole run is set by Plan.Duration. If over is longer than Duration, to is never reached.
func Rate ¶
Rate starts scenarios at a rate of n per per. For example, Rate(10, time.Second) is 10 per second.
func Stages ¶
Stages runs several stages in order. The Arrival of each stage runs with the start of that stage as 0 (a Ramp's slope also starts there). For example, ramp up to 50/s over 30 seconds, hold for 60 seconds, then ramp down to 0 over 10 seconds:
Stages(
Stage{Ramp(0, 50, time.Second, 30*time.Second), 30 * time.Second},
Stage{Rate(50, time.Second), 60 * time.Second},
Stage{Ramp(50, 0, time.Second, 10*time.Second), 10 * time.Second},
)
Arrivals stop when all stages are over. The length of the whole run is set by Plan.Duration, so make it match the sum of the For of every stage.
type ExpectError ¶
type ExpectError struct {
// Want is the status code that was expected.
Want int
// Got is the status code that came back.
Got int
}
ExpectError reports that the status code did not match what ExpectStatus expected.
func (*ExpectError) Error ¶
func (e *ExpectError) Error() string
Error implements the error interface.
type FeedMode ¶
type FeedMode int
FeedMode is the order in which a Feeder hands out rows.
const ( // Circular hands out rows from the first one and wraps around after the last. Circular FeedMode = iota // Random picks a row at random each time. The same row can be picked twice in a row. Random // Once hands out rows from the first one and returns false once they are used up. // It is meant for data such as accounts that can be used only once. Once )
type Feeder ¶
type Feeder[T any] struct { // contains filtered or unexported fields }
Feeder hands out different data (accounts, input values, ...) to each user. Next may be called from all users at the same time.
Example ¶
A Feeder hands out different data to each user.
package main
import (
"fmt"
"github.com/RIRUa/jotaro"
)
func main() {
accounts := jotaro.NewFeeder([]map[string]string{
{"user": "alice"}, {"user": "bob"},
}, jotaro.Circular)
for range 3 {
row, _ := accounts.Next()
fmt.Println(row["user"])
}
}
Output: alice bob alice
Example (Once) ¶
With Once, rows are handed out once and then Next returns false.
package main
import (
"fmt"
"github.com/RIRUa/jotaro"
)
func main() {
tokens := jotaro.NewFeeder([]string{"t1", "t2"}, jotaro.Once)
for {
tok, ok := tokens.Next()
if !ok {
break
}
fmt.Println(tok)
}
}
Output: t1 t2
type Plan ¶
type Plan struct {
// Scenario is the flow every virtual user runs.
Scenario *Scenario
// BaseURL is prepended to the relative paths of requests (for example "http://localhost:8080").
BaseURL string
// Users is the number of virtual users running at the same time.
Users int
// Arrival is how scenarios arrive (e.g. Rate(10, time.Second)). It cannot be set together with Users.
Arrival Arrival
// MaxUsers is the cap on concurrency with Arrival. 0 means 1000.
// Arrivals that come in while the cap is reached are not run and are counted as dropped.
MaxUsers int
// After Duration has passed, no new iterations are started.
Duration time.Duration
// Timeout is the limit for a single request. 0 means 30 seconds.
Timeout time.Duration
// Grace is how long to wait for running iterations to finish after Duration.
// Those that exceed it are aborted and recorded as record.ErrCanceled. 0 means 30 seconds.
Grace time.Duration
// If Raw is not nil, the raw data is written as JSON Lines when Run finishes (record.WriteRaw).
// Even if writing fails, the aggregated Report is returned (together with an error).
Raw io.Writer
}
Plan describes how load is applied.
Specify exactly one of Users (closed model) and Arrival (open model).
- Users: a fixed number of users repeat the scenario until Duration has passed.
- Arrival: scenarios start at a fixed rate, regardless of how long a scenario takes.
Example (Open) ¶
In the open model, scenarios start at a fixed rate whatever the server does. Stages switches the arrival stage by stage: ramp up, hold, ramp down.
package main
import (
"time"
"github.com/RIRUa/jotaro"
)
func main() {
var scenario *jotaro.Scenario // see the Example above
plan := jotaro.Plan{
Scenario: scenario,
BaseURL: "http://localhost:8080",
Duration: 100 * time.Second,
Arrival: jotaro.Stages(
jotaro.Stage{Arrival: jotaro.Ramp(0, 50, time.Second, 30*time.Second), For: 30 * time.Second},
jotaro.Stage{Arrival: jotaro.Rate(50, time.Second), For: 60 * time.Second},
jotaro.Stage{Arrival: jotaro.Ramp(50, 0, time.Second, 10*time.Second), For: 10 * time.Second},
),
MaxUsers: 500, // arrivals beyond this are counted as dropped
}
_ = plan // jotaro.Run(ctx, plan)
}
Output:
type Request ¶
type Request struct {
// Spec is the data form of the request. It can be edited directly before Do.
Spec RequestSpec
// contains filtered or unexported fields
}
Request is a request builder. Send it with Do.
Builder methods return the Request so calls can be chained. A failure while building (for example JSON that cannot be encoded) is stored and returned later by the Response, which keeps the chain free of error checks.
func (*Request) Body ¶
Body sets the body according to its type.
[]byte / string sent as is. Content-Type is not set; add it with Header if you need it io.Reader read to the end and sent as is. Content-Type is not set url.Values same as Form anything else same as JSON (structs, maps, slices, ...)
nil sets nothing. Use JSON when you want to make it explicit that the body is JSON (also when you want to send a string as a JSON string).
func (*Request) Do ¶
Do sends the request. A network error is kept in the response and returned by Decode or ExpectStatus.
type RequestSpec ¶
type RequestSpec struct {
Method string
Path string
Query url.Values // added to the query of Path
Header http.Header
Body []byte
}
RequestSpec describes a request. It is kept as data separate from execution so that it can later be built from YAML and the like.
type Response ¶
type Response struct {
// Code is the HTTP status code. It is 0 when no response was received.
Code int
// Header holds the response headers. It is nil when no response was received.
Header http.Header
// Body is the whole body, already read.
Body []byte
// contains filtered or unexported fields
}
Response is the result of a single request. HTTP 4xx/5xx are not network errors, so Err stays nil and the status goes into Code.
func (*Response) Err ¶
Err returns the network error, if any. It does not look at the HTTP status code.
func (*Response) ExpectStatus ¶
ExpectStatus checks that the status code equals code.
type Scenario ¶
type Scenario struct {
// Name labels the scenario in every record and in the report.
Name string
// Fn is the flow itself. Returning an error marks the iteration as failed.
Fn func(ctx context.Context, u *User) error
}
Scenario is the flow that a virtual user runs once.
type Stage ¶
type Stage struct {
// Arrival decides when scenarios start within this stage, counted from the stage's own start.
Arrival Arrival
// For is how long the stage lasts. Arrivals that would fall at or after For are dropped.
For time.Duration
}
Stage is one stage of Stages. It keeps Arrival going for For.
type Step ¶
type Step struct {
// contains filtered or unexported fields
}
Step is the entry point for building requests inside u.Step. Every request built from it is recorded under this step's name.
type User ¶
type User struct {
// ID identifies the virtual user. In the open model every arrival gets its own ID.
ID int
// Iteration counts how many times this user has run the scenario (0-based).
Iteration int
// Client sends the requests. Use it directly for anything the Step builders do not cover,
// but note that such requests are not recorded.
Client *http.Client
// contains filtered or unexported fields
}
User is a virtual user that runs the scenario once. ID is the number of the virtual user and does not change across iterations. Client has its own cookie jar per iteration, so every iteration starts a fresh session.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
jotaro
command
Command jotaro is a CLI that works with result files saved by the jotaro library.
|
Command jotaro is a CLI that works with result files saved by the jotaro library. |
|
examples
|
|
|
checkout
command
Command checkout is a sample that shows how to use jotaro.
|
Command checkout is a sample that shows how to use jotaro. |
|
marketplace
command
Command marketplace is a deeper sample than examples/checkout.
|
Command marketplace is a deeper sample than examples/checkout. |
|
internal
|
|
|
histogram
Package histogram is the log histogram (DDSketch style) that Latency holds for merging.
|
Package histogram is the log histogram (DDSketch style) that Latency holds for merging. |
|
mcpserver
Package mcpserver is the MCP server behind "jotaro mcp".
|
Package mcpserver is the MCP server behind "jotaro mcp". |
|
Package record holds the raw data of a load test (records of requests, steps and iterations), and its input and output.
|
Package record holds the raw data of a load test (records of requests, steps and iterations), and its input and output. |
|
Package report aggregates and outputs the results (Report) of a load test.
|
Package report aggregates and outputs the results (Report) of a load test. |