Skip to main content

Command Palette

Search for a command to run...

Grafana xk6

Updated
4 min read
Grafana xk6

最近因為任務,需要對公司的平台做 smoke testing 與一些基本的 load testing。但因為我們是 Web3 的產品有點麻煩,登入要 OTP 驗證,還有很多簽章的流程需要處理。剛好又是做借貸撮合的平台,需要開多個瀏覽器登入不同角色跟帳號進行操作。剛好 k6 有提供 xk6 來建置出 k6 的插件。

除此之外這測試腳本還是能跟原本的 k6 http load testing 腳本混合執行。同時能執行瀏覽器的測試以及 API 的壓測的 solution 不算多,k6 算是其中之一。

但這篇先來寫 xk6 .


Using k6 browser | Grafana k6 documentation

Grafana k6 Browser previos version

k6 Browser 本來也是 k6 的插件之一,也是用 xk6 來編譯使用的,但到了 k6 0.56 版本後,就被合併到 k6 的主要程式庫當中了。剛好在寫文章當下也是最新版本也是 0.56

Starting from k6 version v0.56, this codebase has been merged and is now part of the main k6 repository.

但我們能看一下它原本是怎麼透過 xk6 編譯的。根目錄中的 Makefile 中的 build job.

go install go.k6.io/xk6/cmd/xk6@latest && 
xk6 build --output xk6-browser --with github.com/grafana/xk6-browser=.

xk6

https://github.com/grafana/xk6 xk6 主要是利用 Go 語言的 package 管理器來進行套件的下載與編譯出 k6 套件的, 所以套件開發者需要安裝 Go 才方便編譯 xk6 撰寫的套件.

** 安裝 xk6 **

# Install xk6
go install go.k6.io/xk6/cmd/xk6@latest

然後用 Go 指令新建一個專案, 這裡我用簡單的 OTP 產生器為例子. 以下是檔案目錄.

├── otp
│   ├── go.mod
│   ├── go.sum
│   └── otp.go

接著需要在專案中安裝k6套件.

go get go.k6.io/k6

最重要的是modules.Register("k6/x/otp", new(OtpGenerator)), 這裡第一個參數一定要事 k6/x/ 來作為k6模組的名稱前綴. x 我不知道是experimental 還是 xk6的 x 就是了.

** 模組架構設計 (Go 語言部分)**

  1. 模組註冊與生命週期
// 全局模組 root 實例 (每個測試程序只會存在一個)
type RootModule struct{} 

// 模組實例 (每個 VU 虛擬用戶獨立一個實例)
type ModuleInstance struct {
    vu modules.VU  // 持有 VU 上下文
}

// 必須實現的 interface 驗證
var (
    _ modules.Module   = &RootModule{}
    _ modules.Instance = &ModuleInstance{}
)
  1. 模組初始化流程
func init() {
    // 註冊模組到 k6 系統 (強制前綴 k6/x/)
    modules.Register("k6/x/otp", new(RootModule))
}

// 創建 VU 級實例
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
    return &ModuleInstance{vu: vu}
}

// 匯出 JS 可調用的構建函數
func (m *ModuleInstance) Exports() modules.Exports {
    return modules.Exports{
        Named: map[string]interface{}{
            "OtpGenerator": m.newOtpGenerator, // JS 中 new OtpGenerator()
        },
    }
}

OTP 產生器實現細節 3. 核心類結構

type OtpGenerator struct {
    vu       modules.VU  // 必備的 VU 引用
    secret   string      //  密鑰
    passcode string      // 最後生成的驗證碼
}
  1. JavaScript 綁定 (構建函數)
func (m *ModuleInstance) newOtpGenerator(c sobek.ConstructorCall) *sobek.Object {
    rt := m.vu.Runtime()

    // 參數驗證邏輯
    if len(c.Arguments) != 1 {
        common.Throw(rt, fmt.Errorf("需要 1 個參數 (secret)"))
    }
    secret := c.Argument(0).String()
    if secret == "" || len(secret) < 16 {
        common.Throw(rt, fmt.Errorf("無效密鑰"))
    }

    // 創建 Go 實例
    generator := &OtpGenerator{
        vu:     m.vu,
        secret: secret,
    }

    // 創建 JS 對象並綁定方法
    obj := rt.NewObject()
    obj.Set("generate", generator.generate)       // 綁定產生 OTP 方法
    obj.Set("getPasscode", generator.getPasscode) // 綁定取值方法
    return obj
}

xk6 編譯與使用

cd otp && xk6 build \
    --with github.com/$(basename $(pwd))=. \
    --output ../k6

檢查編譯出來的 k6 執行檔其包含的模組資訊. 我這裡出現兩個是因為我編譯時 --with 有用到另一個模組.

./k6 version

k6 v0.56.0 (go1.22.1, darwin/arm64)
Extensions:
  github.com (devel), k6/x/kv [js]
  github.com (devel), k6/x/otp [js]

多模組同時編譯的指令

cd otp && xk6 build \
    --with github.com/$(basename $(pwd))=. \
    --with github.com/oleiade/xk6-kv \
    --output ../k6

也能透過 docker image 來協助編譯. 其中grafana/xk6 build v0.56, 是用來指定用 v0.56版本的 k6 來編譯.

docker run --rm -it -e GOOS=darwin  -u "$(id -u):$(id -g)" \
    -v "${PWD}:/xk6" \
    grafana/xk6 build v0.56 \
    --with github.com/demo/otp=./otp \
    --with github.com/oleiade/xk6-kv

以下是完整程式

package otp

import (
    "fmt"
    "time"

    "github.com/grafana/sobek"
    "github.com/pquerna/otp"
    "github.com/pquerna/otp/totp"
    "go.k6.io/k6/js/common"
    "go.k6.io/k6/js/modules"
)

// Initialize module registration
func init() {
    modules.Register("k6/x/otp", new(RootModule))
}

// RootModule implements the global module instance
type RootModule struct{}

// ModuleInstance represents a per-VU module instance
type ModuleInstance struct {
    vu modules.VU
}

// Ensure interfaces are implemented
var (
    _ modules.Module   = &RootModule{}
    _ modules.Instance = &ModuleInstance{}
)

// NewModuleInstance creates a new module instance for each VU
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
    return &ModuleInstance{
        vu: vu,
    }
}

// Exposes constructor functions for JavaScript binding
func (m *ModuleInstance) Exports() modules.Exports {
    return modules.Exports{
        Named: map[string]interface{}{
            "OtpGenerator": m.newOtpGenerator,
        },
    }
}

// OtpGenerator handles TOTP generation operations
type OtpGenerator struct {
    vu       modules.VU
    secret   string // Base32 encoded secret key
    passcode string // Last generated passcode
}

// newOtpGenerator constructor creates JS-bound OTP generator object
func (m *ModuleInstance) newOtpGenerator(c sobek.ConstructorCall) *sobek.Object {
    rt := m.vu.Runtime()

    // Validate constructor arguments
    if len(c.Arguments) != 1 {
        // 統一使用 common.Throw 拋出異常
        common.Throw(rt, fmt.Errorf("OtpGenerator requires 1 argument (secret)"))
    }
    secret := c.Argument(0).String()
    if secret == "" {
        common.Throw(rt, fmt.Errorf("secret cannot be empty"))
    }
    if len(secret) < 16 {
        common.Throw(rt, fmt.Errorf("secret too short"))
    }

    // Initialize generator instance
    generator := &OtpGenerator{
        vu:     m.vu,
        secret: secret,
    }

    // Create JS object and bind methods
    obj := rt.NewObject()

    // Bind generate method
    if err := obj.Set("generate", generator.generate); err != nil {
        common.Throw(rt, err)
    }

    // Bind passcode getter (optional)
    if err := obj.Set("getPasscode", func() string {
        return generator.passcode
    }); err != nil {
        common.Throw(rt, err)
    }

    return obj
}

// generate handles TOTP code generation with optional timestamp parameter
// Returns generated passcode directly to JS
func (g *OtpGenerator) generate(c sobek.FunctionCall) sobek.Value {
    rt := g.vu.Runtime()

    // Handle timestamp parameter
    var now time.Time
    if len(c.Arguments) > 0 {
        // JavaScript -> Go 參數接收
        ms := c.Argument(0).ToInteger()
        now = time.UnixMilli(ms)
    } else {
        now = time.Now()
    }

    // Generate TOTP with fixed parameters
    passcode, err := totp.GenerateCodeCustom(g.secret, now, totp.ValidateOpts{
        Period:    30, // 30-second intervals
        Skew:      1,  // Allow 1 interval before/after
        Digits:    otp.DigitsSix,
        Algorithm: otp.AlgorithmSHA1,
    })

    if err != nil {
        common.Throw(rt, err)
    }

    // Store and return the generated code
    g.passcode = passcode

   // Go -> JavaScript 值轉換
    return rt.ToValue(passcode)
}

** k6 客戶端使用**

import { OtpGenerator } from 'k6/x/otp';

const generator = new OtpGenerator(otpSecret);
const code = generator.generate();

總結

在公司專案中使用 k6 時, 基本上有很大機會會透過 xk6 來編譯出客製化的 k6 套件. 偏偏官網這部分講的不是太豐富. 就把應用到的部分簡單整理.

下一篇再來分享 k6 Browser.

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read164
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read75
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read190
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read461
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts