Skip to content

Commit

Permalink
feat: 🎸 add user-gray plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
heimanba committed Jul 11, 2024
1 parent c40cf85 commit 7dd4ed4
Show file tree
Hide file tree
Showing 8 changed files with 474 additions and 0 deletions.
112 changes: 112 additions & 0 deletions plugins/wasm-go/extensions/user-gray/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# user-gray 前端灰度插件
## 功能说明
`user-gray`插件实现了前端用户灰度的的功能,通过此插件,不但可以用于业务`A/B实验`,同时通过`可灰度`配合`可监控`,`可回滚`策略保证系统发布运维的稳定性。

## 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
| `uid-key` | string | 必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid |
| `uid-sub-key` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`uid-sub-key`取值为`userCode` |
| `rules` | array of map | 非必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
| `deploy` | map of map | 非必填 | - | 分别配置Base基线和Gary灰度的生效规则,以及生效版本 |

`rules`字段配置说明:

| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
| `name` | string | 必填 | - | 规则名称唯一标识,和`deploy.gray[].name`进行关联生效 |
| `uid-value` | array of string | 非必填 | - | 用户ID 白名单列表 |
| `gray-tag-key` | string | 非必填 | - | 用户分类打标的标签key值,来自Cookie |
| `gray-tag-value` | array of string | 非必填 | - | 用户分类打标的标签value值,来自Cookie |


`deploy`字段配置说明:

| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
| `base` | map of string | 必填 | - | 定义Base版本,如果匹配不到灰度版本,默认fallback到当前版本 |
| `gray` | array of string | 非必填 | - | 定义Gray版本,如果匹配到灰度规则,则当前的灰度版本生效 |

`deploy.base`字段配置说明:

| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------|--------------|------|-----|-----------------------------------------------------------------------------------|
| `version` | string | 必填 | - | Base版本的版本号,作为兜底的版本 |

`deploy.gray`字段配置说明:

| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|--------|--------|------|-----|----------------------------|
| `version` | string | 必填 | - | Gray版本的版本号,如果命中灰度规则,则使用此版本 |
| `name` | string | 必填 | - | 规则名称和`rules[].name`关联, |
| `enable` | boolean | 必填 | - | 是否启动当前灰度规则 |

## 配置示例
### 基础配置
```yml
uid-key: userid
rules:
- name: inner-user
uid-value:
- '00000001'
- '00000005'
- name: beta-user
uid-value:
- '00000002'
- '00000003'
gray-tag-key: level
gray-tag-value:
- level3
- level5
deploy:
base:
version: base
gray:
- name: beta-user
version: gray
enable: true
```
cookie中的用户唯一标识为 `userid`,当前灰度规则配置了`beta-user`的规则。

当满足下面调试的时候,会使用`versin: gray`版本
- cookie中`userid`等于`00000002`或者`00000003`
- cookie中`level`等于`level3`或者`level5`的用户

否则使用`versin: base`版本

### 用户信息存在JSON中

```yml
uid-key: appInfo
uid-sub-key: userId
rules:
- name: inner-user
uid-value:
- '00000001'
- '00000005'
- name: beta-user
uid-value:
- '00000002'
- '00000003'
gray-tag-key: level
gray-tag-value:
- level3
- level5
deploy:
base:
version: base
gray:
- name: beta-user
version: gray
enable: true
```

cookie存在`appInfo`的JSON数据,其中包含`userId`字段为当前的唯一标识
当前灰度规则配置了`beta-user`的规则。
当满足下面调试的时候,会使用`versin: gray`版本
- cookie中`userid`等于`00000002`或者`00000003`
- cookie中`level`等于`level3`或者`level5`的用户

否则使用`versin: base`版本
90 changes: 90 additions & 0 deletions plugins/wasm-go/extensions/user-gray/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package config

import (
"strconv"

"github.com/tidwall/gjson"
)

type GrayRule struct {
Name string
UidValue []interface{}
GrayTagKey string
GrayTagValue []interface{}
}

type DeployItem struct {
Name string
Version string
Enable bool
}

type Deploy struct {
Base *DeployItem
Gray []*DeployItem
}

type UserGrayConfig struct {
UidKey string
UidSubKey string
Rules []*GrayRule
Deploy *Deploy
}

func interfacesFromJSONResult(results []gjson.Result) []interface{} {
var interfaces []interface{}
for _, result := range results {
switch v := result.Value().(type) {
case float64:
// 当 v 是 float64 时,将其转换为字符串
interfaces = append(interfaces, strconv.FormatFloat(v, 'f', -1, 64))
default:
// 其它类型不改变,直接追加
interfaces = append(interfaces, v)
}
}
return interfaces
}

func JsonToUserGrayConfig(json gjson.Result, userGrayConfig *UserGrayConfig) {
// 解析 UidKey
userGrayConfig.UidKey = json.Get("uid-key").String()
userGrayConfig.UidSubKey = json.Get("uid-sub-key").String()

// 解析 Rules
rules := json.Get("rules").Array()
for _, rule := range rules {
grayRule := GrayRule{
Name: rule.Get("name").String(),
UidValue: interfacesFromJSONResult(rule.Get("uid-value").Array()), // 使用辅助函数将 []gjson.Result 转换为 []interface{}
GrayTagKey: rule.Get("gray-tag-key").String(),
GrayTagValue: interfacesFromJSONResult(rule.Get("gray-tag-value").Array()),
}
userGrayConfig.Rules = append(userGrayConfig.Rules, &grayRule)
}

// 解析 deploy
deployJSON := json.Get("deploy")
baseItem := deployJSON.Get("base")
grayItems := deployJSON.Get("gray").Array()

// 分配内存给 release 对象
userGrayConfig.Deploy = &Deploy{
Base: &DeployItem{
Name: baseItem.Get("name").String(),
Version: baseItem.Get("version").String(),
Enable: baseItem.Get("enable").Bool(),
},
Gray: []*DeployItem{},
}

// 解析 Gray 列表
for _, item := range grayItems {
DeployItem := &DeployItem{
Name: item.Get("name").String(),
Version: item.Get("version").String(),
Enable: item.Get("enable").Bool(),
}
userGrayConfig.Deploy.Gray = append(userGrayConfig.Deploy.Gray, DeployItem)
}
}
27 changes: 27 additions & 0 deletions plugins/wasm-go/extensions/user-gray/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package config

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)

func TestJsonToUserGreyConfig(t *testing.T) {
allConfigData := `{"uid-key":"userid","rules":[{"name":"inner-user","uid-value":["00000001","00000005"]},{"name":"beta-user","uid-value":["00000002","00000003"],"gray-tag-key":"level","gray-tag-value":["level3","level5"]}],"deploy":{"base":{"version":"base"},"gray":[{"name":"beta-user","version":"gray","enable":true}]}}`
var tests = []struct {
testName string
uidKey string
json string
}{
{"完整的数据", "userid", allConfigData},
}
for _, test := range tests {
testName := test.testName
t.Run(testName, func(t *testing.T) {
var userGrayConfig = &UserGrayConfig{}
JsonToUserGrayConfig(gjson.Parse(test.json), userGrayConfig)
assert.Equal(t, test.uidKey, userGrayConfig.UidKey)
})
}
}
22 changes: 22 additions & 0 deletions plugins/wasm-go/extensions/user-gray/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module user-gray

go 1.18

require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.17.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/resp v0.1.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
32 changes: 32 additions & 0 deletions plugins/wasm-go/extensions/user-gray/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e h1:0b2UXrEpotHwWgwvgvkXnyKWuxTXtzfKu6c2YpRV+zw=
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20240531060402-2807ddfbb79e/go.mod h1:10jQXKsYFUF7djs+Oy7t82f4dbie9pISfP9FJwpPLuk=
github.com/alibaba/higress/plugins/wasm-go v1.3.5 h1:VOLL3m442IHCSu8mR5AZ4sc6LVT9X0w1hdqDI7oB9jY=
github.com/alibaba/higress/plugins/wasm-go v1.3.5/go.mod h1:kr3V9Ntbspj1eSrX8rgjBsdMXkGupYEf+LM72caGPQc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226065437-8f7a0b3c9071 h1:STb5rOHRZOzoiAa+gTz2LFqO1nYj7U/1eIVUJJadU4A=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226065437-8f7a0b3c9071/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
79 changes: 79 additions & 0 deletions plugins/wasm-go/extensions/user-gray/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package main

import (
"user-gray/config"
"user-gray/util"

"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)

func main() {
wrapper.SetCtx(
"user-gray",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}

func parseConfig(json gjson.Result, userGrayConfig *config.UserGrayConfig, log wrapper.Log) error {
// 解析json 为UserGrayConfig
config.JsonToUserGrayConfig(json, userGrayConfig)
return nil
}

// OmitGrayRule 过滤灰度规则
func OmitGrayRule(userGrayConfig *config.UserGrayConfig, userId string) *config.DeployItem {
for _, grayItem := range userGrayConfig.Deploy.Gray {
if !grayItem.Enable {
// 跳过Enable=false
continue
}
grayRule := util.ContainsRule(userGrayConfig.Rules, grayItem.Name)
// 首先:先校验用户名单ID
if grayRule.UidValue != nil && len(grayRule.UidValue) > 0 && userId != "" {
if util.Contains(grayRule.UidValue, userId) {
return grayItem
}
}
// 第二:校验Cookie中的 GrayTagKey
if grayRule.GrayTagKey != "" && grayRule.GrayTagValue != nil && len(grayRule.GrayTagValue) > 0 {
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
grayTagValue := util.GetValueByCookie(cookieStr, grayRule.GrayTagKey)
if util.Contains(grayRule.GrayTagValue, grayTagValue) {
return grayItem
}
}
}
return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.UserGrayConfig, log wrapper.Log) types.Action {
// 优先从cookie中获取,如果拿不到再从header中获取
cookieStr, _ := proxywasm.GetHttpRequestHeader("cookie")
uidHeaderKey, _ := proxywasm.GetHttpRequestHeader(grayConfig.UidKey)
uidKeyValue := util.GetValueByCookie(cookieStr, grayConfig.UidKey)
proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
// 优先从Cookie中获取,否则从header中获取
if uidKeyValue == "" {
uidKeyValue = uidHeaderKey
}
// 如果有子key, 尝试从子key中获取值
if grayConfig.UidSubKey != "" {
uidSubKeyValue := util.GetUidFromSubKey(uidKeyValue, grayConfig.UidSubKey)
if uidSubKeyValue != "" {
uidKeyValue = uidSubKeyValue
}
}
grayDeployItem := OmitGrayRule(&grayConfig, uidKeyValue)
if grayDeployItem != nil {
log.Infof("x-mse-tag: %s, userCode: %s", grayDeployItem.Version, uidKeyValue)
proxywasm.AddHttpRequestHeader("x-mse-tag", grayDeployItem.Version)
} else {
log.Infof("x-mse-tag: %s, userCode: %s", grayConfig.Deploy.Base.Version, uidKeyValue)
}

return types.ActionContinue
}
Loading

0 comments on commit 7dd4ed4

Please sign in to comment.