Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/event based handling response#4 #5

Merged
merged 10 commits into from
Feb 17, 2025
39 changes: 39 additions & 0 deletions examples/eventlistener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Client } from 'k6/x/net/http';

export default async function () {
const client = new Client({
proxy: 'http://127.0.0.1:1080',
headers: { 'User-Agent': 'saniyar' }, // set some global headers
});

// client.on('requestToBeSent', event => {
// const request = event.data;
// if (!requestID && request.url == 'https://httpbin.test.k6.io/get?name=k6'
// && request.method == 'GET') {
// // The request ID is a UUIDv4 string that uniquely identifies a single request.
// // This is a contrived check and example, but you can imagine that in a complex
// // script there would be many similar requests.
// requestID = request.id;
// }
// });

client.on('responseReceived', async response => {
console.log(await response.json())
console.log(response.request.id)
console.log(response.id)
// const response = event.data;
// if (requestID && response.request.id == requestID) {
// // Change the request duration metric to any value
// response.metrics['http_req_duration'].value = 3.1415;
// // Consider the request successful regardless of its response
// response.metrics['http_req_failed'].value = false;
// // Or drop a single metric
// delete response.metrics['http_req_duration'];
// // Or drop all metrics
// response.metrics = {};
// }
});

await client.get('https://httpbin.test.k6.io/get');
console.log("first")
}
65 changes: 61 additions & 4 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"net/http"

"github.com/google/uuid"
"github.com/grafana/sobek"
"github.com/saniyar-dev/xk6-new-http/pkg/helpers"
"github.com/saniyar-dev/xk6-new-http/pkg/interfaces"
Expand All @@ -23,13 +24,17 @@ type Client struct {
// The http.Client struct to have all the functionalities of a http.Client in Client struct
http.Client

id string

// Multiple vus in k6 can create multiple Client objects so we need to have access the vu Runtime, etc.
Vu modules.VU

M map[string]sobek.Value

// Params is the way to config the global params for Client object to do requests.
params *Clientparams

eventListeners interfaces.EventListeners
}

var _ interfaces.Object = &Client{}
Expand Down Expand Up @@ -64,18 +69,60 @@ func (c *Client) Set(k string, val sobek.Value) bool {
// Define func defines data properties on obj attatched to Client struct.
func (c *Client) Define() error {
rt := c.Vu.Runtime()
c.eventListeners = (&eventListeners{}).New()
c.id = uuid.New().String()

c.Set("get", rt.ToValue(c.getAsync))
c.Set("on", rt.ToValue(c.addEventListener))
return nil
}

// this function add an eventListener of type t and an fn callback to the object eventListeners
func (c *Client) addEventListener(t string, fn func(sobek.Value) (sobek.Value, error)) error {
el, err := c.eventListeners.GetListener(t)
if err != nil {
return err
}
el.Add(fn)

return nil
}

// this function call eventListeners of any type
func (c *Client) callEventListeners(t string, obj *sobek.Object) error {
el, err := c.eventListeners.GetListener(t)
if err != nil {
return err
}
for _, fn := range el.All() {
if _, err := fn(obj); err != nil {
return err
}
}

return nil
}

// this function queueResponse for handling on the main event loop that the VU has
func (c *Client) queueResponse(resp *response.Response) {
rt := c.Vu.Runtime()
enqCallback := c.Vu.RegisterCallback()

go func() {
enqCallback(func() error {
return c.callEventListeners(RESPONSE, rt.NewDynamicObject(resp))
})
}()
}

// this function would handle any type of request and do the actuall job of requesting
func (c *Client) do(req *request.Request) (*response.Response, error) {
rt := c.Vu.Runtime()

resp := &response.Response{
Vu: c.Vu,
M: make(map[string]sobek.Value),
Vu: c.Vu,
M: make(map[string]sobek.Value),
Request: req,
}

httpResp, err := c.Do(req.Request)
Expand All @@ -84,12 +131,16 @@ func (c *Client) do(req *request.Request) (*response.Response, error) {
}

resp.Response = httpResp

helpers.Must(rt, resp.Define())

c.queueResponse(resp)

return resp, nil
}

// this function would handle creating request with params from input
func (c *Client) createRequest(method string, arg sobek.Value, body io.Reader) (*request.Request, error) {
rt := c.Vu.Runtime()
// add default options to requests function
addDefault := func(req *request.Request) {
for k, vlist := range c.params.headers {
Expand All @@ -110,7 +161,13 @@ func (c *Client) createRequest(method string, arg sobek.Value, body io.Reader) (

if v, ok := arg.Export().(string); ok {
r, err := http.NewRequest(method, v, body)
req := &request.Request{Request: r}
req := &request.Request{
Vu: c.Vu,
M: make(map[string]sobek.Value),
Request: r,
}
helpers.Must(rt, req.Define())

addDefault(req)
return req, err
}
Expand Down
54 changes: 54 additions & 0 deletions pkg/client/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package client

import (
"fmt"

"github.com/saniyar-dev/xk6-new-http/pkg/events"
"github.com/saniyar-dev/xk6-new-http/pkg/helpers"
"github.com/saniyar-dev/xk6-new-http/pkg/interfaces"
)

const (
OPEN = "open"
CLOSE = "close"
ERROR = "error"
RESPONSE = "responseReceived"
REQUEST = "requestToBeSent"
)

type eventListeners struct {
open *events.EventListener
close *events.EventListener
error *events.EventListener
response *events.EventListener
request *events.EventListener
}

var _ interfaces.EventListeners = &eventListeners{}

func (es *eventListeners) GetListener(t string) (*events.EventListener, error) {
switch t {
case OPEN:
return es.open, nil
case CLOSE:
return es.close, nil
case ERROR:
return es.error, nil
case RESPONSE:
return es.response, nil
case REQUEST:
return es.request, nil
default:
return nil, fmt.Errorf("unsupported event type for client %s", t)
}
}

func (es *eventListeners) New() interfaces.EventListeners {
return &eventListeners{
open: helpers.NewEventListener(OPEN),
close: helpers.NewEventListener(CLOSE),
error: helpers.NewEventListener(ERROR),
response: helpers.NewEventListener(RESPONSE),
request: helpers.NewEventListener(REQUEST),
}
}
16 changes: 16 additions & 0 deletions pkg/events/events.go
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
package events

import "github.com/grafana/sobek"

type EventListener struct {
EventType string

List []func(sobek.Value) (sobek.Value, error)
}

func (e *EventListener) Add(fn func(sobek.Value) (sobek.Value, error)) {
e.List = append(e.List, fn)
}

func (e *EventListener) All() []func(sobek.Value) (sobek.Value, error) {
return e.List
}
8 changes: 8 additions & 0 deletions pkg/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/grafana/sobek"
"github.com/saniyar-dev/xk6-new-http/pkg/events"
"go.k6.io/k6/js/common"
)

Expand Down Expand Up @@ -46,3 +47,10 @@ func DynamicRead(read func([]byte) (int, error), timeout time.Duration) (int, []

return total, buffer.Bytes(), nil
}

// NewEventListener function helps you to create a fress EventListener
func NewEventListener(t string) *events.EventListener {
return &events.EventListener{
EventType: t,
}
}
10 changes: 9 additions & 1 deletion pkg/interfaces/interfaces.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package interfaces

import "github.com/grafana/sobek"
import (
"github.com/grafana/sobek"
"github.com/saniyar-dev/xk6-new-http/pkg/events"
)

// Params interface defines the common behavior/functionalities each object params exported on the main API should have.
// Client, Request, TCP, etc. are objects and params you pass to them while initializing them are object params.
Expand All @@ -13,3 +16,8 @@ type Object interface {
Define() error
ParseParams(*sobek.Runtime, []sobek.Value) (Params, error)
}

type EventListeners interface {
GetListener(string) (*events.EventListener, error)
New() EventListeners
}
18 changes: 10 additions & 8 deletions pkg/request/request.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package request

import (
"log"
"net/http"

"github.com/google/uuid"
"github.com/grafana/sobek"
"github.com/saniyar-dev/xk6-new-http/pkg/interfaces"
"go.k6.io/k6/js/modules"
Expand All @@ -13,6 +13,8 @@ import (
type Request struct {
*http.Request

id string

Vu modules.VU

M map[string]sobek.Value
Expand Down Expand Up @@ -49,15 +51,15 @@ func (r *Request) Set(k string, val sobek.Value) bool {
return true
}

func (r *Request) print() {
log.Print("from print function")
}

// Define func defines data properties on obj attatched to Request struct.
func (r *Request) Define() error {
rt := r.Vu.Runtime()
r.Request.URL = r.params.url
r.Request.Header = r.params.headers.Clone()
r.Set("print", rt.ToValue(r.print))

r.id = uuid.New().String()
if r.params != nil {
r.Request.URL = r.params.url
r.Request.Header = r.params.headers.Clone()
}
r.Set("id", rt.ToValue(r.id))
return nil
}
1 change: 1 addition & 0 deletions pkg/response/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ type Responseparams struct{}
var _ interfaces.Params = &Responseparams{}

func (r *Response) ParseParams(rt *sobek.Runtime, args []sobek.Value) (interfaces.Params, error) {
r.params = &Responseparams{}
return &Responseparams{}, nil
}
11 changes: 11 additions & 0 deletions pkg/response/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ import (
"net/http"
"time"

"github.com/google/uuid"
"github.com/grafana/sobek"
"github.com/saniyar-dev/xk6-new-http/pkg/helpers"
"github.com/saniyar-dev/xk6-new-http/pkg/interfaces"
"github.com/saniyar-dev/xk6-new-http/pkg/request"
"go.k6.io/k6/js/modules"
)

// Response object
type Response struct {
*http.Response

id string

Vu modules.VU

M map[string]sobek.Value

Request *request.Request

params *Responseparams
}

var _ interfaces.Object = &Response{}
Expand Down Expand Up @@ -53,8 +61,11 @@ func (r *Response) Set(k string, val sobek.Value) bool {
// Define func defines data properties on obj attatched to Client struct.
func (r *Response) Define() error {
rt := r.Vu.Runtime()
r.id = uuid.New().String()

r.Set("json", rt.ToValue(r.jsonAsync))
r.Set("request", rt.NewDynamicObject(r.Request))
r.Set("id", rt.ToValue(r.id))
return nil
}

Expand Down