利用 Tengo 实现动态配置管理

2023年 8月 14日 53.0k 0

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

可以遗憾,但不要后悔。 

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

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

业务背景

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

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

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

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

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

Tengo 介绍

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

Tengo 是什么

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

Playground 体验

可以在 Playground 中体验 Tengo:tengolang.com/

Tengo 应用场景

动态配置和规则引擎

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

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

  • 动态性:Tengo 允许你在运行时动态修改配置和规则,无需重新启动应用程序。这适用于需要频繁调整和测试的场景,使得实时配置和规则调整成为可能。
  • 灵活性:Tengo 提供了一种脚本化的方式来修改配置和定义规则。通过编写 Tengo 脚本,你可以利用条件逻辑、循环和自定义函数来处理配置和规则,使得配置和规则的定义过程更加灵活和可控。
  • 安全性:Tengo 提供了受控的脚本执行环境,可以限制脚本的功能和访问权限,确保配置和规则的安全性。相比直接操作操作系统环境变量,使用 Tengo 提供更多的安全保障。
  • 可扩展性:Tengo 是一个可扩展的脚本语言,可以与应用程序逻辑集成。你可以在 Tengo 脚本中定义自定义函数和数据结构,更好地处理和管理配置和规则,根据应用需求进行定制和扩展。
  • 总而言之,使用 Tengo 实现动态配置管理和规则引擎提供了更多的灵活性、控制和扩展性。无论是配置的实时调整,还是复杂规则的定义和执行,Tengo 都为这些需求提供了高效和灵活的解决方案。同时,Tengo 的性能和易用性也使得它在实际项目中应用广泛,适用于多种复杂和灵活的配置和规则管理需求。

    工作流引擎

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

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

    Tengo 快速开始

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

    可作为独立语言执行

    全局安装

    go install github.com/d5/tengo/v2
    

    交互模式

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

    ➜ tengo
    >> a := "foo"
    foo
    >> b := -19.84
    -19.84
    >> c := 5
    5
    >> d := true
    true
    

    运行脚本

  • 编写 demo.tengo 脚本
  • // 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)
    
  • 使用 Tengo 解释器直接解释执行
  • ➜ tengo demo.tengo
    a: foo
    b: -19.84
    c: 5
    d: true
    
  • 也支持先编译二进制文件再执行
  • ➜ 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
    

    代码示例

    目录结构:

    .
    ├── base.tengo
    └── demo.tengo
    

    base.tengo 脚本:

    // base.tengo
    fn := func() {
      return {
        a: true,
        b: [1, 2, 3],
        c: {d: "ddd", e: "eee"}
      }
    }
    
    // 将命名空间导出供其他脚本使用
    export {
      data: fn(),
    }
    

    demo.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
    

    执行脚本:

    ➜ tengo demo.tengo
    true
    1
    ddd
    

    使用 Go 的编译或运行时

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

    库安装

    go get github.com/d5/tengo/v2
    

    代码示例

    使用 Tengo 计算数组元素之和

    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 进行表达式求值

    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 的值来决定是否连接到数据库,而不需要使应用程序做重启或退出。这种方案能够提高应用的灵活性和实时性,满足动态配置需求。

    目录结构

    .
    ├── config
    │   ├── base.tengo
    │   ├── dev.tengo
    │   └── prod.tengo
    └── main.go
    

    代码实现

    config/base.tengo 文件

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

    config/dev.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 文件

    // 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 主文件

    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?&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-dynamic-configuration.gif

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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论