利用 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 作为脚本引擎的一个非常基础的初步尝试。在实际的生产环境中,我们并不建议基于灵活性而采用这样的设计来切换生产和测试库!

相关推荐
程序员大金1 小时前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
程序员大金1 小时前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
Pandaconda1 小时前
【计算机网络 - 基础问题】每日 3 题(十)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Ylucius2 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
ღ᭄ꦿ࿐Never say never꧂3 小时前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
.生产的驴3 小时前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
海里真的有鱼3 小时前
Spring Boot 中整合 Kafka
后端