Custom guns
Warning
New Documentation https://yandex.github.io/pandora/
Basic tutorial
You can create you own Golang-based gun with pandora.
There is an example of custom gun shooting via gRPC.
We create a new gun and define `shoot`
method for it w/ our test logic.
- You can find examples of custom guns’ code below:
Now it’s time to compile our gun. Install deps and compile your custom gun file (`go build my_custom_gun.go`
).
After that step you’ll get `my_custom_gun`
binary file, it is compiled pandora with your custom gun inside.
Now its time to create `load.yaml`
:
pools:
- id: HTTP pool
gun:
type: my_custom_gun_name # custom gun name (specified at `register.Gun("my_custom_gun_name", ...`)
target: "your_grpc_host:your_grpc_port"
ammo:
type: custom_provider
source:
type: file
path: ./json.ammo
result:
type: phout
destination: ./phout.log
rps: {duration: 30s, type: line, from: 1, to: 2}
startup:
type: once
times: 10
log:
level: error
And create ammofile `./json.ammo`
:
{"tag": "/MyCase1", "Param1": "146837693,146837692,146837691"}
{"tag": "/MyCase2", "Param2": "555", "Param1": "500002"}
We are ready to shoot. Try it.
gRPC
// create a package
package main
// import some pandora stuff
// and stuff you need for your scenario
// and protobuf contracts for your grpc service
import (
"log"
"context"
"strconv"
"strings"
"time"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/satori/go.uuid"
"github.com/spf13/afero"
"google.golang.org/grpc"
pb "my_package/my_protobuf_contracts"
"github.com/yandex/pandora/cli"
"github.com/yandex/pandora/components/phttp/import"
"github.com/yandex/pandora/core"
"github.com/yandex/pandora/core/aggregator/netsample"
"github.com/yandex/pandora/core/import"
"github.com/yandex/pandora/core/register"
)
type Ammo struct {
Tag string
Param1 string
Param2 string
Param3 string
}
type Sample struct {
URL string
ShootTimeSeconds float64
}
type GunConfig struct {
Target string `validate:"required"` // Configuration will fail, without target defined
}
type Gun struct {
// Configured on construction.
client grpc.ClientConn
conf GunConfig
// Configured on Bind, before shooting
aggr core.Aggregator // May be your custom Aggregator.
core.GunDeps
}
func NewGun(conf GunConfig) *Gun {
return &Gun{conf: conf}
}
func (g *Gun) Bind(aggr core.Aggregator, deps core.GunDeps) error {
// create gRPC stub at gun initialization
conn, err := grpc.Dial(
g.conf.Target,
grpc.WithInsecure(),
grpc.WithTimeout(time.Second),
grpc.WithUserAgent("load test, pandora custom shooter"))
if err != nil {
log.Fatalf("FATAL: %s", err)
}
g.client = *conn
g.aggr = aggr
g.GunDeps = deps
return nil
}
func (g *Gun) Shoot(ammo core.Ammo) {
customAmmo := ammo.(*Ammo)
g.shoot(customAmmo)
}
func (g *Gun) case1_method(client pb.MyClient, ammo *Ammo) int {
code := 0
// prepare list of ids from ammo
var itemIDs []int64
for _, id := range strings.Split(ammo.Param1, ",") {
if id == "" {
continue
}
itemID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
log.Printf("Ammo parse FATAL: %s", err)
code = 314
}
itemIDs = append(itemIDs, itemID)
}
out, err := client.GetSomeData(
context.TODO(), &pb.ItemsRequest{
itemIDs})
if err != nil {
log.Printf("FATAL: %s", err)
code = 500
}
if out != nil {
code = 200
}
return code
}
func (g *Gun) case2_method(client pb.MyClient, ammo *Ammo) int {
code := 0
// prepare item_id and warehouse_id
item_id, err := strconv.ParseInt(ammo.Param1, 10, 0)
if err != nil {
log.Printf("Failed to parse ammo FATAL", err)
code = 314
}
warehouse_id, err2 := strconv.ParseInt(ammo.Param2, 10, 0)
if err2 != nil {
log.Printf("Failed to parse ammo FATAL", err2)
code = 314
}
items := []*pb.SomeItem{}
items = append(items, &pb.SomeItem{
item_id,
warehouse_id,
1,
×tamp.Timestamp{time.Now().Unix(), 111}
})
out2, err3 := client.GetSomeDataSecond(
context.TODO(), &pb.SomeRequest{
uuid.Must(uuid.NewV4()).String(),
1,
items})
if err3 != nil {
log.Printf("FATAL", err3)
code = 316
}
if out2 != nil {
code = 200
}
return code
}
func (g *Gun) shoot(ammo *Ammo) {
code := 0
sample := netsample.Acquire(ammo.Tag)
conn := g.client
client := pb.NewClient(&conn)
switch ammo.Tag {
case "/MyCase1":
code = g.case1_method(client, ammo)
case "/MyCase2":
code = g.case2_method(client, ammo)
default:
code = 404
}
defer func() {
sample.SetProtoCode(code)
g.aggr.Report(sample)
}()
}
func main() {
//debug.SetGCPercent(-1)
// Standard imports.
fs := afero.NewOsFs()
coreimport.Import(fs)
// May not be imported, if you don't need http guns and etc.
phttp.Import(fs)
// Custom imports. Integrate your custom types into configuration system.
coreimport.RegisterCustomJSONProvider("custom_provider", func() core.Ammo { return &Ammo{} })
register.Gun("my_custom_gun_name", NewGun, func() GunConfig {
return GunConfig{
Target: "default target",
}
})
cli.Run()
}
Websockets
package main
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"math/rand"
"mime/multipart"
"net/http"
"net/url"
"strconv"
"time"
"github.com/gorilla/websocket"
"github.com/spf13/afero"
"github.com/yandex/pandora/cli"
"github.com/yandex/pandora/components/phttp/import"
"github.com/yandex/pandora/core"
"github.com/yandex/pandora/core/aggregator/netsample"
"github.com/yandex/pandora/core/import"
"github.com/yandex/pandora/core/register"
)
type Ammo struct {
Tag string
}
type Sample struct {
URL string
ShootTimeSeconds float64
}
type GunConfig struct {
Target string `validate:"required"`
Handler string `validate:"required"`// Configuration will fail, without target defined
}
type Gun struct {
// Configured on construction.
client websocket.Conn
conf GunConfig
// Configured on Bind, before shooting
aggr core.Aggregator // May be your custom Aggregator.
core.GunDeps
}
func NewGun(conf GunConfig) *Gun {
return &Gun{conf: conf}
}
func (g *Gun) Bind(aggr core.Aggregator, deps core.GunDeps) error {
targetPath := url.URL{Scheme: "ws", Host: g.conf.Target, Path: g.conf.Handler}
sample := netsample.Acquire("connection")
code := 0
rand.Seed(time.Now().Unix())
conn, _, err := websocket.DefaultDialer.Dial(
targetPath.String(),
nil,
)
if err != nil {
log.Fatalf("dial err FATAL %s:", err)
code = 500
} else {
code = 200
}
g.client = *conn
g.aggr = aggr
g.GunDeps = deps
defer func() {
sample.SetProtoCode(code)
g.aggr.Report(sample)
}()
go func() {
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("read:", err)
code = 400
return
}
log.Printf("recv: %s", message)
}
}()
err = conn.WriteMessage(websocket.TextMessage, []byte("some websocket connection initialization text, e.g. token"))
if err != nil {
log.Println("write:", err)
}
return nil
}
func (g *Gun) Shoot(ammo core.Ammo) {
sample := netsample.Acquire("message")
code := 0
conn := g.client
err := conn.WriteMessage(websocket.TextMessage, []byte("test_message"))
if err != nil {
log.Println("connection closed", err)
code = 600
} else {
code = 200
}
func() {
sample.SetProtoCode(code)
g.aggr.Report(sample)
}()
}
func main() {
//debug.SetGCPercent(-1)
// Standard imports.
fs := afero.NewOsFs()
coreimport.Import(fs)
// May not be imported, if you don't need http guns and etc.
phttp.Import(fs)
// Custom imports. Integrate your custom types into configuration system.
coreimport.RegisterCustomJSONProvider("ammo_provider", func() core.Ammo { return &Ammo{} })
register.Gun("my_custom_gun_name", NewGun, func() GunConfig {
return GunConfig{
Target: "default target",
}
})
cli.Run()
}