Skip to content

Conversation

@aresnasa
Copy link

Nightingale BaseURL/BasePath 功能分析文档

概述

Nightingale v6 已支持通过 BasePath 配置进行反向代理部署,允许在非根路径上运行应用。本文档记录了完整的实现细节,用于向 nightingale 社区提交改进 PR。

核心实现

1. 配置定义 (pkg/httpx/httpx.go)

type Config struct {
    Host             string
    Port             int
    BasePath         string // Base path prefix for reverse proxy deployment, e.g., "/nightingale"
    // ... 其他配置项
}

功能:允许在配置文件中指定应用的基础路径前缀。

2. 路径规范化函数 (pkg/httpx/httpx.go)

// NormalizedBasePath returns the normalized base path.
// It ensures the path starts with "/" and does not end with "/".
// Returns empty string if BasePath is not set.
func (c *Config) NormalizedBasePath() string {
    if c.BasePath == "" {
        return ""
    }
    basePath := strings.TrimSuffix(c.BasePath, "/")
    if !strings.HasPrefix(basePath, "/") {
        basePath = "/" + basePath
    }
    return basePath
}

功能

  • 确保路径以 / 开头
  • 确保路径不以 / 结尾(移除末尾斜杠)
  • 如果 BasePath 为空返回空字符串

示例

  • "/nightingale"/nightingale
  • "nightingale"/nightingale
  • "/nightingale/"/nightingale
  • """"

3. Gin 引擎初始化 (pkg/httpx/httpx.go)

func GinEngine(mode string, cfg Config, printBodyPaths func() map[string]struct{},
    printAccessLog func() bool) *gin.Engine {
    // ... 初始化代码
    
    basePath := cfg.NormalizedBasePath()

    if cfg.PProf {
        pprof.Register(r, basePath+"/api/debug/pprof")
    }

    r.GET(basePath+"/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })

    r.GET(basePath+"/pid", func(c *gin.Context) {
        c.String(200, fmt.Sprintf("%d", os.Getpid()))
    })

    r.GET(basePath+"/ppid", func(c *gin.Context) {
        c.String(200, fmt.Sprintf("%d", os.Getppid()))
    })

    r.GET(basePath+"/addr", func(c *gin.Context) {
        c.String(200, c.Request.RemoteAddr)
    })

    r.GET(basePath+"/api/n9e/version", func(c *gin.Context) {
        c.String(200, version.Version)
    })

    if cfg.ExposeMetrics {
        r.GET(basePath+"/metrics", gin.WrapH(promhttp.Handler()))
    }

    return r
}

覆盖的路由

  • {basePath}/ping - 健康检查
  • {basePath}/pid - 获取进程 ID
  • {basePath}/ppid - 获取父进程 ID
  • {basePath}/addr - 获取远程地址
  • {basePath}/api/n9e/version - 获取版本信息
  • {basePath}/metrics - Prometheus 指标(可选)
  • {basePath}/api/debug/pprof - 性能分析(可选)

4. 中心路由配置 (center/router/router.go)

4.1 静态文件服务

basePath := rt.HTTP.NormalizedBasePath()

if !rt.Center.UseFileAssets {
    r.StaticFS(basePath+"/pub", statikFS)
}

4.2 API 路由前缀

pagesPrefix := basePath + "/api/n9e"
pages := r.Group(pagesPrefix)
{
    pages.DELETE("/datasource/series", rt.auth(), rt.admin(), rt.deleteDatasourceSeries)
    pages.Any("/proxy/:id/*url", rt.dsProxy)
    pages.POST("/query-range-batch", rt.promBatchQueryRange)
    // ... 更多路由
}

4.3 前端路由处理 (configNoRoute)

func (rt *Router) configNoRoute(r *gin.Engine, fs *http.FileSystem, basePath string) {
    r.NoRoute(func(c *gin.Context) {
        requestPath := c.Request.URL.Path

        // If basePath is set, strip it from the request path for static file matching
        if basePath != "" {
            if strings.HasPrefix(requestPath, basePath) {
                requestPath = strings.TrimPrefix(requestPath, basePath)
                if requestPath == "" {
                    requestPath = "/"
                }
            } else {
                // Request path doesn't start with basePath, return 404
                c.String(404, "Not Found")
                return
            }
        }

        // 处理静态资源和 SPA 路由
        arr := strings.Split(requestPath, ".")
        suffix := arr[len(arr)-1]

        switch suffix {
        case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map", "ttf", "md":
            // 处理静态资源
            if !rt.Center.UseFileAssets {
                c.FileFromFS(requestPath, *fs)
            } else {
                // 从文件系统获取
            }
        default:
            // 对于 SPA,默认返回 index.html
            if !rt.Center.UseFileAssets {
                c.FileFromFS("/", *fs)
            } else {
                // 从文件系统获取 index.html
            }
        }
    })
}

关键点

  • 处理带 basePath 前缀的请求
  • 从请求路径中移除 basePath 前缀以匹配文件
  • 对于不存在的路由,返回 index.html(支持 SPA)
  • 如果请求路径不以 basePath 开头,返回 404

5. Alert 路由配置 (alert/router/router.go)

type Router struct {
    HTTP httpx.Config
    // ... 其他字段
}

func (rt *Router) Config(r *gin.Engine) {
    if !rt.HTTP.APIForService.Enable {
        return
    }

    basePath := rt.HTTP.NormalizedBasePath()
    service := r.Group(basePath + "/v1/n9e")
    if len(rt.HTTP.APIForService.BasicAuth) > 0 {
        service.Use(gin.BasicAuth(rt.HTTP.APIForService.BasicAuth))
    }
    service.POST("/event", rt.pushEventToQueue)
    service.POST("/event-persist", rt.eventPersist)
    service.POST("/make-event", rt.makeEvent)
}

6. Pushgw 路由配置 (pushgw/router/router.go)

func (rt *Router) Config(r *gin.Engine) {
    basePath := rt.HTTP.NormalizedBasePath()

    service := r.Group(basePath + "/v1/n9e")
    if len(rt.HTTP.APIForService.BasicAuth) > 0 {
        service.Use(gin.BasicAuth(rt.HTTP.APIForService.BasicAuth))
    }
    service.POST("/target-update", rt.targetUpdate)

    if !rt.HTTP.APIForAgent.Enable {
        return
    }

    r.Use(stat())
    // Datadog API 端点 - 都带 basePath 前缀
    r.POST(basePath+"/datadog/api/v1/series", rt.datadogSeries)
    r.POST(basePath+"/datadog/api/v1/check_run", datadogCheckRun)
    r.GET(basePath+"/datadog/api/v1/validate", datadogValidate)
    r.POST(basePath+"/datadog/api/v1/metadata", datadogMetadata)
    r.POST(basePath+"/datadog/intake/", datadogIntake)
    // ... 其他路由
}

配置示例

YAML 配置

http:
  # ... 其他配置
  basePath: "/monitoring"  # 或 "monitoring",都会被规范化为 /monitoring

# 或者
http:
  basePath: "/nightingale/"  # 末尾的 / 会被移除

使用场景

  1. Nginx 反向代理

    location /monitoring {
        proxy_pass http://nightingale:8080;
    }

    此时配置 basePath: "/monitoring"

  2. 子域名反向代理

    http://monitoring.example.com -> basePath: "" (不需要配置)
    
  3. 容器编排平台

    • 可以根据环境变量动态设置 basePath

改进建议(PR 建议)

1. 前端 BaseURL 传递机制

当前状态:后端支持 basePath,但前端可能需要知道该值以正确构建 API 请求。

建议

// 在初始化时将 basePath 作为全局配置返回给前端
r.GET(basePath+"/api/n9e/config", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "basePath": basePath,
        // ... 其他配置
    })
})

前端可以从该端点获取 basePath,然后用于构建所有 API 请求。

2. 文档改进

  • 在 README 中添加反向代理部署的完整示例
  • 提供 Nginx、Apache、Traefik 等常见反向代理的配置示例
  • 说明前端需要如何处理 basePath

3. 中间件支持

可以创建一个中间件来自动处理 basePath:

func basePathMiddleware(basePath string) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("basePath", basePath)
        c.Next()
    }
}

4. 测试覆盖

需要添加测试用例来验证:

  • basePath 规范化的各种情况
  • 带 basePath 的静态文件服务
  • SPA 路由在 basePath 下的工作
  • 404 处理

相关文件总结

文件 说明
pkg/httpx/httpx.go 配置定义和规范化函数
center/router/router.go 中心路由配置和 SPA 处理
alert/router/router.go Alert 服务路由配置
pushgw/router/router.go Pushgw 服务路由配置

总结

Nightingale v6 已经具有完整的 basePath 支持,主要包括:

  1. ✅ 配置定义和规范化
  2. ✅ 后端路由前缀添加
  3. ✅ 静态文件服务支持
  4. ✅ SPA 路由处理
  5. ✅ 多服务(center、alert、pushgw)支持
  6. ⚠️ 需要改进:前端与后端的 basePath 同步机制
  7. ⚠️ 需要改进:更完善的文档和示例

提交 PR 的要点

  1. 问题陈述:前端无法自动获知 basePath,需要手动配置
  2. 解决方案:添加配置端点或通过 HTML 模板注入 basePath
  3. 测试:提供完整的集成测试示例
  4. 文档:更新 README 和部署指南

生成时间:2025-12-15
用途:向 nightingale 社区提交 PR 的参考文档

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant