Skip to content

Commit

Permalink
[TT-1722] Update WASP docs (#1340)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tofel authored Dec 6, 2024
1 parent 989c0a5 commit 1449c13
Show file tree
Hide file tree
Showing 21 changed files with 259 additions and 84 deletions.
10 changes: 8 additions & 2 deletions wasp/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type AlertChecker struct {
grafanaClient *grafana.Client
}

// NewAlertChecker creates a new AlertChecker using Grafana configurations from environment variables.
// It retrieves GRAFANA_URL and GRAFANA_TOKEN, ensuring they are set.
// Use this function to set up alert checking in tests.
func NewAlertChecker(t *testing.T) *AlertChecker {
url := os.Getenv("GRAFANA_URL")
if url == "" {
Expand All @@ -43,7 +46,8 @@ func NewAlertChecker(t *testing.T) *AlertChecker {
}
}

// AnyAlerts check if any alerts with dashboardUUID have been raised
// AnyAlerts retrieves alert groups from Grafana and checks for alerts matching the specified dashboard UUID and requirement label value.
// It returns the matching alert groups, enabling users to identify and respond to specific alert conditions.
func (m *AlertChecker) AnyAlerts(dashboardUUID, requirementLabelValue string) ([]grafana.AlertGroupsResponse, error) {
raised := false
defer func() {
Expand Down Expand Up @@ -75,7 +79,9 @@ func (m *AlertChecker) AnyAlerts(dashboardUUID, requirementLabelValue string) ([
return alertGroups, nil
}

// CheckDashobardAlerts checks for alerts in the given dashboardUUIDs between from and to times
// CheckDashboardAlerts retrieves alert annotations from a Grafana dashboard within the specified time range.
// It returns the sorted alerts and an error if any alert is in the alerting state.
// Use it to verify the status of dashboard alerts after a run.
func CheckDashboardAlerts(grafanaClient *grafana.Client, from, to time.Time, dashboardUID string) ([]grafana.Annotation, error) {
annotationType := "alert"
alerts, _, err := grafanaClient.GetAnnotations(grafana.AnnotationsQueryParams{
Expand Down
7 changes: 5 additions & 2 deletions wasp/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ type SliceBuffer[T any] struct {
Data []T
}

// NewSliceBuffer creates new limited capacity slice
// NewSliceBuffer creates a new SliceBuffer with the specified capacity.
// It provides an efficient way to store and manage a fixed number of elements,
// enabling optimized access and manipulation in concurrent and decentralized applications.
func NewSliceBuffer[T any](cap int) *SliceBuffer[T] {
return &SliceBuffer[T]{Capacity: cap, Data: make([]T, 0)}
}

// Append appends T if len <= cap, overrides old data otherwise
// Append adds an element to the SliceBuffer. When the buffer reaches its capacity, it overwrites the oldest item.
// This function is useful for maintaining a fixed-size, circular collection of elements.
func (m *SliceBuffer[T]) Append(s T) {
if m.Idx >= m.Capacity {
m.Idx = 0
Expand Down
9 changes: 6 additions & 3 deletions wasp/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import (
"github.com/rs/zerolog/log"
)

// ExecCmd executes os command, logging both streams
// ExecCmd executes the provided command string and logs its output.
// It returns an error if the command fails to run or exits with a non-zero status.
func ExecCmd(command string) error {
return ExecCmdWithStreamFunc(command, func(m string) {
log.Info().Str("Text", m).Msg("Command output")
})
}

// readStdPipe continuously read a pipe from the command
// readStdPipe reads lines from the provided pipe and sends each line to streamFunc.
// It is used to handle streaming output from command execution, such as stdout and stderr.
func readStdPipe(pipe io.ReadCloser, streamFunc func(string)) {
scanner := bufio.NewScanner(pipe)
scanner.Split(bufio.ScanLines)
Expand All @@ -28,7 +30,8 @@ func readStdPipe(pipe io.ReadCloser, streamFunc func(string)) {
}
}

// ExecCmdWithStreamFunc executes command with stream function
// ExecCmdWithStreamFunc runs the specified command and streams its output and error lines
// to the provided outputFunction. It enables real-time handling of command execution output.
func ExecCmdWithStreamFunc(command string, outputFunction func(string)) error {
c := strings.Split(command, " ")
cmd := exec.Command(c[0], c[1:]...)
Expand Down
2 changes: 2 additions & 0 deletions wasp/dashboard/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"github.com/smartcontractkit/chainlink-testing-framework/wasp/dashboard"
)

// main creates and deploys a default dashboard using environment variables for configuration.
// It sets up the dashboard without extensions or non-functional requirements, enabling straightforward deployment.
func main() {
// just default dashboard, no NFRs, no dashboard extensions
// see examples/alerts.go for an example extension
Expand Down
38 changes: 28 additions & 10 deletions wasp/dashboard/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type Dashboard struct {
builder dashboard.Builder
}

// NewDashboard creates new dashboard
// NewDashboard initializes a Dashboard with provided alerts and options, using environment variables for configuration. It prepares the dashboard for deployment and returns the instance or an error if setup fails.
func NewDashboard(reqs []WaspAlert, opts []dashboard.Option) (*Dashboard, error) {
name := os.Getenv("DASHBOARD_NAME")
if name == "" {
Expand Down Expand Up @@ -93,7 +93,8 @@ func NewDashboard(reqs []WaspAlert, opts []dashboard.Option) (*Dashboard, error)
return dash, nil
}

// Deploy deploys this dashboard to some Grafana folder
// Deploy uploads the Dashboard to Grafana, creating the folder if necessary.
// It returns the deployed grabana.Dashboard and any encountered error.
func (m *Dashboard) Deploy() (*grabana.Dashboard, error) {
ctx := context.Background()
client := grabana.NewClient(&http.Client{}, m.GrafanaURL, grabana.WithAPIToken(m.GrafanaToken))
Expand All @@ -105,7 +106,8 @@ func (m *Dashboard) Deploy() (*grabana.Dashboard, error) {
return client.UpsertDashboard(ctx, fo, m.builder)
}

// defaultStatWidget creates default Stat widget
// defaultStatWidget creates a standard dashboard stat widget using the specified name, datasource, Prometheus target, and legend.
// It is used to display consistent metrics within dashboard rows.
func defaultStatWidget(name, datasourceName, target, legend string) row.Option {
return row.WithStat(
name,
Expand All @@ -120,7 +122,8 @@ func defaultStatWidget(name, datasourceName, target, legend string) row.Option {
)
}

// defaultLastValueAlertWidget creates default last value alert
// defaultLastValueAlertWidget generates a timeseries.Option for alerting using a WaspAlert.
// It returns the custom alert if provided, otherwise configures a default last-value alert for consistent monitoring in dashboards.
func defaultLastValueAlertWidget(a WaspAlert) timeseries.Option {
if a.CustomAlert != nil {
return a.CustomAlert
Expand All @@ -143,7 +146,9 @@ func defaultLastValueAlertWidget(a WaspAlert) timeseries.Option {
)
}

// defaultLabelValuesVar creates a dashboard variable with All/Multiple options
// defaultLabelValuesVar generates a dashboard variable for the specified name and datasource.
// It enables multiple selections, includes an "All" option, and sorts label values in ascending numerical order.
// Use it to create consistent query variables for dashboard filtering.
func defaultLabelValuesVar(name, datasourceName string) dashboard.Option {
return dashboard.VariableAsQuery(
name,
Expand All @@ -155,7 +160,8 @@ func defaultLabelValuesVar(name, datasourceName string) dashboard.Option {
)
}

// timeSeriesWithAlerts creates timeseries graphs per alert + definition of alert
// timeSeriesWithAlerts creates dashboard options for each WaspAlert, configuring time series panels with alert settings.
// Use it to add alert-specific rows to a dashboard based on provided alert definitions.
func timeSeriesWithAlerts(datasourceName string, alertDefs []WaspAlert) []dashboard.Option {
dashboardOpts := make([]dashboard.Option, 0)
for _, a := range alertDefs {
Expand Down Expand Up @@ -189,6 +195,9 @@ func timeSeriesWithAlerts(datasourceName string, alertDefs []WaspAlert) []dashbo
return dashboardOpts
}

// AddVariables generates standard dashboard options for common label variables using the provided datasourceName.
// It includes variables like go_test_name, gen_name, branch, commit, and call_group.
// Use this to easily incorporate these variables into your dashboard configuration.
func AddVariables(datasourceName string) []dashboard.Option {
opts := []dashboard.Option{
defaultLabelValuesVar("go_test_name", datasourceName),
Expand All @@ -200,7 +209,8 @@ func AddVariables(datasourceName string) []dashboard.Option {
return opts
}

// dashboard is internal appendable representation of all Dashboard widgets
// dashboard generates dashboard configuration options based on the specified datasource and alert requirements.
// It is used to set up panels and settings when building a new dashboard.
func (m *Dashboard) dashboard(datasourceName string, requirements []WaspAlert) []dashboard.Option {
panelQuery := map[string]string{
"branch": `=~"${branch:pipe}"`,
Expand All @@ -221,7 +231,8 @@ func (m *Dashboard) dashboard(datasourceName string, requirements []WaspAlert) [
return defaultOpts
}

// Build creates dashboard instance
// Build initializes the Dashboard with the specified name, data source, and alert requirements.
// It prepares the dashboard builder for further configuration and usage.
func (m *Dashboard) Build(dashboardName, datasourceName string, requirements []WaspAlert) error {
b, err := dashboard.New(
dashboardName,
Expand All @@ -234,12 +245,14 @@ func (m *Dashboard) Build(dashboardName, datasourceName string, requirements []W
return nil
}

// JSON render dashboard as JSON
// JSON serializes the Dashboard into indented JSON format.
// It provides a human-readable representation, useful for exporting or inspecting the dashboard.
func (m *Dashboard) JSON() ([]byte, error) {
return m.builder.MarshalIndentJSON()
}

// InlineLokiAlertParams is specific params for predefined alerts for wasp dashboard
// InlineLokiAlertParams generates a Loki query string based on the alert type, test name, and generator name.
// It is used to configure specific alert conditions for monitoring test metrics in dashboards.
func InlineLokiAlertParams(queryType, testName, genName string) string {
switch queryType {
case AlertTypeQuantile99:
Expand All @@ -262,6 +275,8 @@ max_over_time({go_test_name="%s", test_data_type=~"stats", gen_name="%s"}
}
}

// WASPLoadStatsRow creates a "WASP Load Stats" dashboard row with widgets displaying real-time and total load metrics.
// It utilizes the provided data source and query parameters to configure the relevant statistics for monitoring.
func WASPLoadStatsRow(dataSource string, query map[string]string) dashboard.Option {
queryString := ""
for key, value := range query {
Expand Down Expand Up @@ -384,6 +399,9 @@ func WASPLoadStatsRow(dataSource string, query map[string]string) dashboard.Opti
)
}

// WASPDebugDataRow returns a dashboard.Option containing a row with WASP debug metrics and logs.
// It uses the provided data source and query parameters.
// Use this function to include detailed debug information in your dashboard.
func WASPDebugDataRow(dataSource string, query map[string]string, collapse bool) dashboard.Option {
queryString := ""
for key, value := range query {
Expand Down
5 changes: 5 additions & 0 deletions wasp/dashboard/panels.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"github.com/K-Phoen/grabana/timeseries/axis"
)

// RPSPanel creates a time series panel displaying responses per second
// grouped by generator and call group. It is used to monitor
// response rates in the dashboard.
func RPSPanel(dataSource string, query map[string]string) row.Option {
queryString := ""
for key, value := range query {
Expand Down Expand Up @@ -35,6 +38,8 @@ func RPSPanel(dataSource string, query map[string]string) row.Option {
)
}

// RPSVUPerScheduleSegmentsPanel creates a dashboard panel displaying Requests Per Second and Virtual Users segmented by schedule.
// It is used to monitor performance metrics over time for different test configurations.
func RPSVUPerScheduleSegmentsPanel(dataSource string, query map[string]string) row.Option {
queryString := ""
for key, value := range query {
Expand Down
6 changes: 4 additions & 2 deletions wasp/gun_http_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ type MockHTTPGun struct {
Data []string
}

// NewHTTPMockGun create an HTTP mock gun
// NewHTTPMockGun initializes a MockHTTPGun with the given configuration.
// It sets up the HTTP client and data storage, enabling simulated HTTP interactions for testing.
func NewHTTPMockGun(cfg *MockHTTPGunConfig) *MockHTTPGun {
return &MockHTTPGun{
client: resty.New(),
Expand All @@ -23,7 +24,8 @@ func NewHTTPMockGun(cfg *MockHTTPGunConfig) *MockHTTPGun {
}
}

// Call implements example gun call, assertions on response bodies should be done here
// Call sends an HTTP GET request to the configured target URL and returns the response data.
// It is used to simulate HTTP calls for testing or load generation purposes.
func (m *MockHTTPGun) Call(l *Generator) *Response {
var result map[string]interface{}
r, err := m.client.R().
Expand Down
9 changes: 7 additions & 2 deletions wasp/gun_sleep_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ type MockGun struct {
Data []string
}

// NewMockGun create a mock gun
// NewMockGun creates a new MockGun instance using the provided configuration.
// It is used to simulate gun behavior for testing or development purposes.
func NewMockGun(cfg *MockGunConfig) *MockGun {
return &MockGun{
cfg: cfg,
Data: make([]string, 0),
}
}

// Call implements example gun call, assertions on response bodies should be done here
// Call simulates a request to the Generator.
// Depending on MockGun's configuration, it may succeed, fail, or timeout,
// allowing testing of various response scenarios.
func (m *MockGun) Call(l *Generator) *Response {
if m.cfg.InternalStop {
l.Stop()
Expand All @@ -54,6 +57,8 @@ func (m *MockGun) Call(l *Generator) *Response {
return &Response{Data: "successCallData"}
}

// convertResponsesData extracts successful and failed response data from the Generator.
// It returns a slice of successful response strings, OK responses, and failed responses.
func convertResponsesData(g *Generator) ([]string, []*Response, []*Response) {
g.responsesData.okDataMu.Lock()
defer g.responsesData.okDataMu.Unlock()
Expand Down
7 changes: 7 additions & 0 deletions wasp/http_server_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,24 @@ type HTTPMockServer struct {
Sleep time.Duration
}

// Run starts the HTTPMockServer in a separate goroutine.
// It enables the server to handle incoming HTTP requests concurrently.
func (s *HTTPMockServer) Run() {
go func() {
//nolint
_ = s.srv.Run()
}()
}

// URL returns the base URL of the HTTPMockServer.
// Use it to configure clients to send requests to the mock server during testing.
func (s *HTTPMockServer) URL() string {
return "http://localhost:8080/1"
}

// NewHTTPMockServer initializes an HTTP mock server with configurable latencies and response codes.
// If cfg is nil, default settings are applied.
// Use it to simulate HTTP endpoints for testing purposes.
func NewHTTPMockServer(cfg *HTTPMockServerConfig) *HTTPMockServer {
if cfg == nil {
cfg = &HTTPMockServerConfig{
Expand Down
20 changes: 17 additions & 3 deletions wasp/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ type K8sClient struct {
RESTConfig *rest.Config
}

// GetLocalK8sDeps get local k8s context config
// GetLocalK8sDeps retrieves the local Kubernetes Clientset and REST configuration.
// It is used to initialize a Kubernetes client for interacting with the cluster.
func GetLocalK8sDeps() (*kubernetes.Clientset, *rest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
Expand All @@ -38,7 +39,8 @@ func GetLocalK8sDeps() (*kubernetes.Clientset, *rest.Config, error) {
return k8sClient, k8sConfig, nil
}

// NewK8sClient creates a new k8s client with a REST config
// NewK8sClient initializes and returns a new K8sClient for interacting with the local Kubernetes cluster.
// It is used to perform operations such as synchronizing groups and managing cluster profiles.
func NewK8sClient() *K8sClient {
cs, cfg, err := GetLocalK8sDeps()
if err != nil {
Expand All @@ -50,18 +52,27 @@ func NewK8sClient() *K8sClient {
}
}

// jobPods returns a list of pods in the specified namespace matching the sync label.
// It is used to track and manage job-related pods within Kubernetes environments.
func (m *K8sClient) jobPods(ctx context.Context, nsName, syncLabel string) (*v1.PodList, error) {
return m.ClientSet.CoreV1().Pods(nsName).List(ctx, metaV1.ListOptions{LabelSelector: syncSelector(syncLabel)})
}

// jobs retrieves the list of Kubernetes jobs within the specified namespace
// that match the provided synchronization label.
// It returns a JobList and an error if the operation fails.
func (m *K8sClient) jobs(ctx context.Context, nsName, syncLabel string) (*batchV1.JobList, error) {
return m.ClientSet.BatchV1().Jobs(nsName).List(ctx, metaV1.ListOptions{LabelSelector: syncSelector(syncLabel)})
}

// syncSelector formats a sync label into a label selector string.
// It is used to filter Kubernetes jobs and pods based on the specified synchronization label.
func syncSelector(s string) string {
return fmt.Sprintf("sync=%s", s)
}

// removeJobs deletes all jobs in the given JobList within the specified namespace.
// It is used to clean up job resources after they have completed or failed.
func (m *K8sClient) removeJobs(ctx context.Context, nsName string, jobs *batchV1.JobList) error {
log.Info().Msg("Removing jobs")
for _, j := range jobs.Items {
Expand All @@ -75,6 +86,8 @@ func (m *K8sClient) removeJobs(ctx context.Context, nsName string, jobs *batchV1
return nil
}

// waitSyncGroup waits until the specified namespace has jobNum pods with the given syncLabel running.
// It ensures that all required pods are synchronized and operational before proceeding.
func (m *K8sClient) waitSyncGroup(ctx context.Context, nsName string, syncLabel string, jobNum int) error {
outer:
for {
Expand All @@ -97,7 +110,8 @@ outer:
}
}

// TrackJobs tracks both jobs and their pods until they succeed or fail
// TrackJobs monitors Kubernetes jobs in the specified namespace and label selector until the desired number succeed or a failure occurs.
// It optionally removes jobs upon completion based on the keepJobs flag.
func (m *K8sClient) TrackJobs(ctx context.Context, nsName, syncLabel string, jobNum int, keepJobs bool) error {
log.Debug().Str("LabelSelector", syncSelector(syncLabel)).Msg("Searching for jobs/pods")
for {
Expand Down
7 changes: 6 additions & 1 deletion wasp/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ const (
LogLevelEnvVar = "WASP_LOG_LEVEL"
)

// init initializes the default logging configuration for the package by setting the logging level and output destination.
func init() {
initDefaultLogging()
}

// initDefaultLogging configures the default logger using the LogLevelEnvVar environment variable.
// It sets the logging output to standard error and defaults to the "info" level if the variable is unset.
func initDefaultLogging() {
lvlStr := os.Getenv(LogLevelEnvVar)
if lvlStr == "" {
Expand All @@ -29,7 +32,9 @@ func initDefaultLogging() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(lvl)
}

// GetLogger instantiates a logger that takes into account the test context and the log level
// GetLogger returns a zerolog.Logger configured with the specified component name and log level.
// If a *testing.T is provided, the logger integrates with test output.
// Use it to enable consistent logging across components with environment-based log level control.
func GetLogger(t *testing.T, componentName string) zerolog.Logger {
lvlStr := os.Getenv(LogLevelEnvVar)
if lvlStr == "" {
Expand Down
Loading

0 comments on commit 1449c13

Please sign in to comment.