利用 Tengo 实现动态配置管理

一次 Tengo 动态配置应用初体验

可以遗憾,但不要后悔。

我们留在这里,从来不是身不由己。

--------- 而是选择在这里经历生活

业务背景

在业务不断扩展的背景下,"数据" 和 "计算" 的解耦需求逐渐凸显。计算规则的灵活性变化,因此将其直接嵌入主应用程序变得不太适宜,因此我们需要一款强大的规则引擎或动态配置平台。

平台的设计考虑到两个主要用户群体:技术人员和非技术人员。

对于非技术用户,我们提供了简单易用的界面和交互方式,以降低他们的学习成本和使用门槛。这可能包括可视化工具、拖放式界面和预定义模板等,旨在简化配置和规则定义过程。

同时,对于技术用户,我们提供了一种灵活且可扩展的方式来自定义和配置规则,以满足他们的需求。这些用户可能更喜欢编写脚本以实现复杂的业务逻辑,以获得更高的灵活性和自定义能力。

无论用户属于哪一类,底层都可以使用一套适用于频繁变更和动态配置的脚本引擎。我们选择了 Tengo 来完成这项任务,它适用于构建自定义脚本引擎、扩展应用程序功能和实现插件系统等多种场景。Tengo 为我们提供了稳定性和灵活性,助力我们有效应对不断变化的业务需求。

Tengo 介绍

Tengo 语言具有类似于 Go 语言的语法和特性,并且被设计用于嵌入到其他应用程序中,以提供脚本化和扩展能力。

Tengo 是什么

  • Tengo 是一门年轻而强大的动态无类型脚本语言。
  • Tengo 的底层依赖于 Golang,兼具简洁易懂的语法和卓越的性能表现。
  • Tengo 具备支持外部变量和函数的特性,能够方便地导入外部资源,同时支持局部变量的定义和控制语句的使用。
  • Tengo 非常适用于作为脚本引擎,在虚拟机的实现方式下,它可以应用于多个应用领域。

Playground 体验

可以在 Playground 中体验 Tengotengolang.com/

Tengo 应用场景

动态配置和规则引擎

Tengo 是一款强大的动态脚本语言,不仅语法简单易用,而且基于 Golang 实现,性能出色。它可以同时作为动态配置管理和规则引擎的理想搭档,为业务规则的定义、执行和管理提供了灵活性和强大的工具。

使用 Tengo 进行动态配置管理和规则引擎的优势如下:

  1. 动态性Tengo 允许你在运行时动态修改配置和规则,无需重新启动应用程序。这适用于需要频繁调整和测试的场景,使得实时配置和规则调整成为可能。
  2. 灵活性Tengo 提供了一种脚本化的方式来修改配置和定义规则。通过编写 Tengo 脚本,你可以利用条件逻辑、循环和自定义函数来处理配置和规则,使得配置和规则的定义过程更加灵活和可控。
  3. 安全性Tengo 提供了受控的脚本执行环境,可以限制脚本的功能和访问权限,确保配置和规则的安全性。相比直接操作操作系统环境变量,使用 Tengo 提供更多的安全保障。
  4. 可扩展性Tengo 是一个可扩展的脚本语言,可以与应用程序逻辑集成。你可以在 Tengo 脚本中定义自定义函数和数据结构,更好地处理和管理配置和规则,根据应用需求进行定制和扩展。

总而言之,使用 Tengo 实现动态配置管理和规则引擎提供了更多的灵活性、控制和扩展性。无论是配置的实时调整,还是复杂规则的定义和执行,Tengo 都为这些需求提供了高效和灵活的解决方案。同时,Tengo 的性能和易用性也使得它在实际项目中应用广泛,适用于多种复杂和灵活的配置和规则管理需求。

工作流引擎

我们的工作流引擎设计中融合了前端的可视化配置和 Tengo 脚本引擎,以满足不同用户的需求。这个设计带来的好处如下:

  1. 简化配置 :对于非技术用户,我们提供直观的拖拽式配置界面,降低了门槛,使他们能够轻松定义和配置工作任务流。用户无需深入了解 Tengo 语言的细节,便能快速创建和管理任务流。
  2. 灵活性 :通过 Tengo 脚本引擎,我们为技术用户提供了强大的自定义能力。他们可以在脚本中使用条件逻辑、循环、自定义函数等功能,以更灵活的方式处理任务逻辑,满足复杂的业务需求。
  3. 实时动态:我们的架构支持在运行时动态修改配置和任务流,无需重新启动应用程序。这使得用户可以实时地对配置进行调整,适用于需要频繁调整和测试配置的场景。
  4. 安全保障 :通过限制 Tengo 脚本的功能和访问权限,我们确保了配置修改和任务执行的安全性。相比直接操作操作系统环境变量,这种方式提供了更多的安全保障。

这个设计方案将前端界面与 Tengo 脚本引擎紧密结合,既满足了非技术用户的需求,又为技术用户提供了自定义和扩展的能力。希望在保持用户友好界面的同时,赋予强大的脚本处理能力,使得任务流配置更加灵活、高效,满足各种复杂的业务场景需求。

Tengo 快速开始

The Tengo Language: Tengo is a small, dynamic, fast, secure script language for Go.

可作为独立语言执行

全局安装

bash 复制代码
go install github.com/d5/tengo/v2

交互模式

直接输入 tengo 进入终端交互命令行

bash 复制代码
➜ tengo
>> a := "foo"
foo
>> b := -19.84
-19.84
>> c := 5
5
>> d := true
true

运行脚本

  1. 编写 demo.tengo 脚本
go 复制代码
// module import
fmt := import("fmt")

// variable definition and primitive types
a := "foo"   // string
b := -19.84  // floating point
c := 5       // integer
d := true    // boolean

fmt.println("a: ", a)
fmt.println("b: ", b)
fmt.println("c: ", c)
fmt.println("d: ", d)
  1. 使用 Tengo 解释器直接解释执行
bash 复制代码
➜ tengo demo.tengo
a: foo
b: -19.84
c: 5
d: true
  1. 也支持先编译二进制文件再执行
bash 复制代码
➜ tengo -o demo demo.tengo
demo

➜ ls -l demo
-rwxr-xr-x  1 mystic  staff  1556  8 13 21:01 demo

➜ tengo demo
a: foo
b: -19.84
c: 5
d: true

代码示例

目录结构:

bash 复制代码
.
├── base.tengo
└── demo.tengo

base.tengo 脚本:

tengo 复制代码
// base.tengo
fn := func() {
  return {
    a: true,
    b: [1, 2, 3],
    c: {d: "ddd", e: "eee"}
  }
}

// 将命名空间导出供其他脚本使用
export {
  data: fn(),
}

demo.tengo 脚本:

tengo 复制代码
// demo.tengo
fmt := import("fmt")
base := import("./base")

fmt.println(base.data.a)      // true
fmt.println(base.data.b[0])   // 1
fmt.println(base.data.c.d)    // ddd

执行脚本:

bash 复制代码
➜ tengo demo.tengo
true
1
ddd

使用 Go 的编译或运行时

下面的示例演示了如何在 Go 代码中使用 Tengo 解释器来执行 Tengo 脚本

库安装

bash 复制代码
go get github.com/d5/tengo/v2

代码示例

使用 Tengo 计算数组元素之和

go 复制代码
package main

import (
    "context"
    "fmt"

    "github.com/d5/tengo/v2"
)

func main() {
    // 创建一个新的 Tengo Script 实例
    script := tengo.NewScript([]byte(
        // Tengo 脚本字符串
        `each := func(seq, fn) {
    for x in seq { fn(x) }
}

sum := 0
each([a, b, c, d], func(x) {
    sum += x
})`))

    // 设置变量值
    _ = script.Add("a", 1)
    _ = script.Add("b", 2)
    _ = script.Add("c", 3)
    _ = script.Add("d", 4)

    // 运行脚本
    compiled, err := script.RunContext(context.Background())
    if err != nil {
        panic(err)
    }

    // 检索值
    sum := compiled.Get("sum")
    fmt.Println(sum) // 打印总和(sum)的值
}

使用 Tengo 进行表达式求值

go 复制代码
package main

import (
    "context"
    "fmt"

    "github.com/d5/tengo/v2"
)

func main() {
    // 创建一个空的上下文
    ctx := context.TODO()

    // 定义 Tengo 表达式并设置变量 input 为 true
    expression := `input ? "success" : "fail"`

    // 设置上下文变量 input 的值为 true
    vars := map[string]any{"input": true}

    // 通过 Tengo.Eval 进行表达式求值
    res, err := tengo.Eval(ctx, expression, vars)
    if err != nil {
        panic(err)
    }

    // 打印结果,应为 "success"
    fmt.Println(res)
}

动态配置案例

需求分析

目标需求如下:

我们通过一个总控脚本(base.tengo)来控制其他两个脚本(dev.tengoprod.tengo)的选项参数。希望实现的效果是根据 currentEnv 变量的值,动态加载不同的脚本。

可以按照以下步骤进行实现:

  • 在总控脚本(base.tengo)中,暴露 currentEnv 选项参数。
  • 在主应用程序(main.go)中读取总控脚本,根据 currentEnv 的值来加载对应的环境配置脚本(dev.tengoprod.tengo)。
  • 另外,通过开启 goroutine 来周期性地更新配置,实现动态切换。

这样的设计允许你根据总控脚本的设定,动态地选择使用哪个数据库配置脚本,并根据 isEnabled 的值来决定是否连接到数据库,而不需要使应用程序做重启或退出。这种方案能够提高应用的灵活性和实时性,满足动态配置需求。

目录结构

bash 复制代码
.
├── config
│   ├── base.tengo
│   ├── dev.tengo
│   └── prod.tengo
└── main.go

代码实现

config/base.tengo 文件

tengo 复制代码
// 当前环境:主应用程序通过该值的变化,选择具体的tengo脚本,并动态加载参数
currentEnv := "dev"    // 或 "prod"

config/dev.tengo 文件

tengo 复制代码
// dev configuration
fmt := import("fmt")

sshCmd := func(isEnabled) {
    sys := {
        user: "root",
        password: "12345",
        ip: "192.168.0.100",
        port: 22
    }
    if isEnabled {
        cmd := fmt.sprintf("sshpass -p '%s' ssh -p %d %s@%s", sys.password, sys.port, sys.user, sys.ip)
        return cmd
    }
    return ""
}(true)

mysql := func(isEnabled) {
    if isEnabled {
        return {
            host: "dev.mysql.com",
            port: 3306,
            username: "dev_user",
            password: "dev_pass",
            dbname: "dev_db"
        }
    }
    return {}
}(true)

redis := func(isEnabled) {
    if isEnabled {
        return {
            host: "dev.redis.com",
            port: 6379,
            password: "12345",
            dbNumber: 0
        }
    }
    return {}
}(true)

// test function
test := func() {
    fmt.println(sshCmd)
    fmt.printf("mysql: %v\n", mysql)
    fmt.printf("redis: %v\n", redis)
}
// test()

config/prod.tengo 文件

tengo 复制代码
// prod configuration
fmt := import("fmt")

sshCmd := func(isEnabled) {
    sys := {
        user: "root",
        password: "admin@123",
        ip: "10.50.0.2",
        port: 10022
    }
    if isEnabled {
        cmd := fmt.sprintf("sshpass -p '%s' ssh -p %d %s@%s", sys.password, sys.port, sys.user, sys.ip)
        return cmd
    }
    return ""
}(true)

mysql := func(isEnabled) {
    if isEnabled {
        return {
            host: "prod.mysql.com",
            port: 13306,
            username: "prod_user",
            password: "prod_pass",
            dbname: "prod_db"
        }
    }
    return {}
}(true)

redis := func(isEnabled) {
    if isEnabled {
        return {
            host: "prod.redis.com",
            port: 16379,
            password: "secret-password",
            dbNumber: 1
        }
    }
    return {}
}(true)

// test function
test := func() {
    fmt.println(sshCmd)
    fmt.printf("mysql: %v\n", mysql)
    fmt.printf("redis: %v\n", redis)
}
// test()

main.go 主文件

go 复制代码
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "strings"
    "time"

    "github.com/d5/tengo/v2"
    "github.com/d5/tengo/v2/stdlib"
)

func loadConfigFile(filename string) ([]byte, error) {
    configData, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return configData, nil
}

func runConfigScript(script *tengo.Script) (*tengo.Compiled, error) {
    compiled, err := script.Run()
    if err != nil {
        return nil, err
    }
    return compiled, nil
}

func execConnection(env string, compiled *tengo.Compiled) {
    // 连接ssh
    func() {
        sshCmd := compiled.Get("sshCmd").Value().(string)

        if sshCmd == "" {
            sshCmd = "unable to ssh connect!"
        }

        log.Printf("| Linux SSH (%s) is connecting | %s\n", env, sshCmd)
    }()

    // 连接mysql
    func() {
        mysql := compiled.Get("mysql").Value().(map[string]any)

        host := mysql["host"]
        port := mysql["port"]
        user := mysql["username"]
        password := mysql["password"]
        dbname := mysql["dbname"]

        var dsn string
        if host == nil || port == nil || user == nil || password == nil || dbname == nil {
            dsn = "unable to mysql connect!"
        } else {
            dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, port, dbname)
        }

        log.Printf("| MySQL DSN (%s) is connecting | %s\n", env, dsn)
    }()

    // 连接redis
    func() {
        redis := compiled.Get("redis").Value().(map[string]any)

        host := redis["host"]
        port := redis["port"]
        password := redis["password"]
        dbNumber := redis["dbNumber"]

        var dsn string
        if host == nil || port == nil || password == nil || dbNumber == nil {
            dsn = "unable to redis connect!"
        } else {
            dsn = fmt.Sprintf("redis://%s@%s:%d/%d", password, host, port, dbNumber)
        }

        log.Printf("| REDIS URL (%s) is connecting | %s\n", env, dsn)
    }()
}

func runConfigUpdater() {
    var i int = 1

    for {
        fmt.Println("轮训监听次数:", i)

        // 加载 base.tengo 文件来获取当前环境配置
        baseConfigData, err := loadConfigFile("config/base.tengo")
        if err != nil {
            log.Fatalf("Error reading base config file:", err)
        }

        // 创建 Tengo 解释器并加载 base.tengo 脚本
        baseScript := tengo.NewScript(baseConfigData)
        baseScript.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
        baseCompiled, err := runConfigScript(baseScript)
        if err != nil {
            log.Fatalf("Error running base config script:", err)
        }
        currentEnv := baseCompiled.Get("currentEnv").Value().(string)

        // 加载当前环境的配置文件
        var envConfigData []byte
        switch strings.ToLower(currentEnv) {
        case "prod":
            envConfigData, err = loadConfigFile("config/prod.tengo")
        case "dev":
            fallthrough
        default:
            envConfigData, err = loadConfigFile("config/dev.tengo")
        }
        if err != nil {
            log.Fatalf("Error reading %s config file:", currentEnv, err)
        }

        // 创建 Tengo 解释器并加载当前环境的配置脚本
        envScript := tengo.NewScript(envConfigData)
        envScript.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
        envCompiled, err := runConfigScript(envScript)
        if err != nil {
            log.Fatalf("Error running config script:", err)
        } else {
            // 执行连接
            execConnection(currentEnv, envCompiled)
        }

        // 每 3 秒重新加载一次 base.tengo 和当前环境的配置文件
        time.Sleep(3 * time.Second)

        i++
    }
}

func main() {
    // 启动周期性的 goroutine 来读取和更新连接参数
    runConfigUpdater()

    // 暂停主 goroutine,以便周期性的 goroutine 继续执行
    select {}
}

效果演示

💡 当然,这仅仅是 Tengo 作为脚本引擎的一个非常基础的初步尝试。在实际的生产环境中,我们并不建议基于灵活性而采用这样的设计来切换生产和测试库!

相关推荐
Victor3561 小时前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack1 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo1 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3561 小时前
MongoDB(3)什么是文档(Document)?
后端
牛奔3 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌8 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX9 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法10 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端