mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
feat(sync): 同步 Wavelet 推送审计、OTel schema 与前端字体
自定义 Webhook 在 HTTP 200 但业务 errcode 非零时记为失败,任务日志记录上游响应。 OTel Resource 改为 NewSchemaless,避免 semconv 与 SDK 版本冲突。 前端用 next/font 自托管 Inter,并忽略浏览器扩展改写 body 引起的 hydration 警告。
This commit is contained in:
@@ -8,6 +8,15 @@ sidebar: false
|
|||||||
|
|
||||||
格式基于 [Keep a Changelog](http://keepachangelog.com/),版本号遵循 [语义化版本](http://semver.org/)。
|
格式基于 [Keep a Changelog](http://keepachangelog.com/),版本号遵循 [语义化版本](http://semver.org/)。
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 🛠 修复
|
||||||
|
- 修复自定义 Webhook 推送在企业微信/钉钉返回 HTTP 200 但 `errcode` 非零时仍记为成功的问题;任务日志会记录上游响应体。
|
||||||
|
- 修复 OpenTelemetry Resource 绑定 semconv schema 版本导致 SDK 升级后可能无法启动的问题。
|
||||||
|
|
||||||
|
### 💄 其他/体验
|
||||||
|
- 前端使用 `next/font` 自托管 Inter,并忽略浏览器扩展改写 `body` 属性引起的 hydration 警告。
|
||||||
|
|
||||||
## 重大变更
|
## 重大变更
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
|
--font-sans: var(--font-inter), "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
--font-mono:
|
--font-mono:
|
||||||
var(--font-geist-mono), "SF Mono", "Monaco", "Inconsolata", "Roboto Mono",
|
var(--font-geist-mono), "SF Mono", "Monaco", "Inconsolata", "Roboto Mono",
|
||||||
"PingFang SC", "Microsoft YaHei", monospace;
|
"PingFang SC", "Microsoft YaHei", monospace;
|
||||||
|
|||||||
+12
-2
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import { ThemeProvider } from '@/components/layout/theme-provider';
|
import { ThemeProvider } from '@/components/layout/theme-provider';
|
||||||
import { CustomThemeProvider } from '@/lib/theme';
|
import { CustomThemeProvider } from '@/lib/theme';
|
||||||
@@ -10,6 +11,12 @@ import { SiteTitleUpdater } from '@/components/providers/title-updater';
|
|||||||
import { RobotsMeta } from '@/components/layout/robots-meta';
|
import { RobotsMeta } from '@/components/layout/robots-meta';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ['latin'],
|
||||||
|
variable: '--font-inter',
|
||||||
|
display: 'swap',
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'OpenFlare',
|
title: 'OpenFlare',
|
||||||
description: 'OpenFlare 边缘节点与反向代理管理平台',
|
description: 'OpenFlare 边缘节点与反向代理管理平台',
|
||||||
@@ -23,10 +30,13 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang='zh-CN'
|
lang='zh-CN'
|
||||||
className='hide-scrollbar font-sans'
|
className={`hide-scrollbar font-sans ${inter.variable}`}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<body className='hide-scrollbar font-sans antialiased'>
|
<body
|
||||||
|
className='hide-scrollbar font-sans antialiased'
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute='class'
|
attribute='class'
|
||||||
defaultTheme='system'
|
defaultTheme='system'
|
||||||
|
|||||||
@@ -52,12 +52,12 @@ type mockPusher struct {
|
|||||||
sentTgt string
|
sentTgt string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPusher) Send(ctx context.Context, cfg pkgpush.Config, target string, body map[string]any, template string, ext map[string]any) error {
|
func (m *mockPusher) Send(ctx context.Context, cfg pkgpush.Config, target string, body map[string]any, template string, ext map[string]any) (string, error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
m.sentBody = body
|
m.sentBody = body
|
||||||
m.sentTgt = target
|
m.sentTgt = target
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPusher) ValidateConfig(cfg pkgpush.Config) error {
|
func (m *mockPusher) ValidateConfig(cfg pkgpush.Config) error {
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ func TestPush(c *gin.Context) {
|
|||||||
keyContent: "当您收到这条消息,说明当前渠道连通性测试通过。",
|
keyContent: "当您收到这条消息,说明当前渠道连通性测试通过。",
|
||||||
keyLevel: defaultLevelInfo,
|
keyLevel: defaultLevelInfo,
|
||||||
}
|
}
|
||||||
if err := pusher.Send(c.Request.Context(), req.Config, req.Target, testBody, "", nil); err != nil {
|
if _, err := pusher.Send(c.Request.Context(), req.Config, req.Target, testBody, "", nil); err != nil {
|
||||||
response.AbortBadRequest(c, err.Error())
|
response.AbortBadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,13 +93,16 @@ func (h *PushHandler) Execute(ctx context.Context, payload []byte) (*task.TaskRe
|
|||||||
|
|
||||||
// 执行真正的消息推送,扁平化为原始 json 格式
|
// 执行真正的消息推送,扁平化为原始 json 格式
|
||||||
flatBody := req.Body.Flatten()
|
flatBody := req.Body.Flatten()
|
||||||
err = pusher.Send(ctx, req.Config, req.Target, flatBody, req.Template, nil)
|
upstreamResp, err := pusher.Send(ctx, req.Config, req.Target, flatBody, req.Template, nil)
|
||||||
|
|
||||||
title := req.Body.Title
|
title := req.Body.Title
|
||||||
content := req.Body.Content
|
content := req.Body.Content
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
task.AppendLog(ctx, "消息推送失败 (标题: %s): %v", title, err)
|
task.AppendLog(ctx, "消息推送失败 (标题: %s): %v", title, err)
|
||||||
|
if upstreamResp != "" {
|
||||||
|
task.AppendLog(ctx, "上游返回: %s", upstreamResp)
|
||||||
|
}
|
||||||
if task.IsFinalAttempt(ctx) {
|
if task.IsFinalAttempt(ctx) {
|
||||||
h.recordHistory(ctx, req, "failed", err.Error())
|
h.recordHistory(ctx, req, "failed", err.Error())
|
||||||
}
|
}
|
||||||
@@ -107,6 +110,9 @@ func (h *PushHandler) Execute(ctx context.Context, payload []byte) (*task.TaskRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
task.AppendLog(ctx, "消息推送成功 (标题: %s, 内容摘要: %s)", title, content)
|
task.AppendLog(ctx, "消息推送成功 (标题: %s, 内容摘要: %s)", title, content)
|
||||||
|
if upstreamResp != "" {
|
||||||
|
task.AppendLog(ctx, "上游返回: %s", upstreamResp)
|
||||||
|
}
|
||||||
h.recordHistory(ctx, req, "success", "")
|
h.recordHistory(ctx, req, "success", "")
|
||||||
|
|
||||||
return &task.TaskResult{
|
return &task.TaskResult{
|
||||||
|
|||||||
+25
-8
@@ -9,6 +9,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -19,13 +20,16 @@ func init() {
|
|||||||
Register("custom", &CustomPusher{})
|
Register("custom", &CustomPusher{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxCustomResponseBytes 限制读取 Webhook 响应体的最大字节数,防止无界读取。
|
||||||
|
const maxCustomResponseBytes = 4096
|
||||||
|
|
||||||
// CustomPusher 自定义 Webhook 发送实现
|
// CustomPusher 自定义 Webhook 发送实现
|
||||||
type CustomPusher struct{}
|
type CustomPusher struct{}
|
||||||
|
|
||||||
// Send 发送自定义 webhook
|
// Send 发送自定义 webhook
|
||||||
func (p *CustomPusher) Send(ctx context.Context, cfg Config, _ string, body map[string]any, template string, _ map[string]any) error {
|
func (p *CustomPusher) Send(ctx context.Context, cfg Config, _ string, body map[string]any, template string, _ map[string]any) (string, error) {
|
||||||
if cfg.URL == "" {
|
if cfg.URL == "" {
|
||||||
return errors.New("custom: URL is required")
|
return "", errors.New("custom: URL is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqBody []byte
|
var reqBody []byte
|
||||||
@@ -39,13 +43,13 @@ func (p *CustomPusher) Send(ctx context.Context, cfg Config, _ string, body map[
|
|||||||
var err error
|
var err error
|
||||||
reqBody, err = json.Marshal(body)
|
reqBody, err = json.Marshal(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("custom: marshal body failed: %w", err)
|
return "", fmt.Errorf("custom: marshal body failed: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.URL, bytes.NewReader(reqBody))
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.URL, bytes.NewReader(reqBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("custom: create http request failed: %w", err)
|
return "", fmt.Errorf("custom: create http request failed: %w", err)
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
@@ -58,15 +62,28 @@ func (p *CustomPusher) Send(ctx context.Context, cfg Config, _ string, body map[
|
|||||||
client := httppool.NewClient(defaultHTTPClientTimeout)
|
client := httppool.NewClient(defaultHTTPClientTimeout)
|
||||||
resp, err := client.Do(httpReq)
|
resp, err := client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("custom: http request failed: %w", err)
|
return "", fmt.Errorf("custom: http request failed: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, maxCustomResponseBytes))
|
||||||
return fmt.Errorf("custom: http status %s", resp.Status)
|
upstreamResp := strings.TrimSpace(string(bodyBytes))
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 { //nolint:mnd
|
||||||
|
return upstreamResp, fmt.Errorf("custom: http status %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// 部分 Webhook(如企业微信、钉钉)即使业务失败也返回 HTTP 200,
|
||||||
|
// 仅当响应体包含非零 errcode 时才判定为发送失败,避免审计记录误报成功。
|
||||||
|
var apiResp struct {
|
||||||
|
ErrCode int `json:"errcode"`
|
||||||
|
ErrMsg string `json:"errmsg"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(bodyBytes, &apiResp); err == nil && apiResp.ErrCode != 0 {
|
||||||
|
return upstreamResp, fmt.Errorf("custom: webhook rejected: errcode=%d errmsg=%q", apiResp.ErrCode, apiResp.ErrMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return upstreamResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateConfig 校验自定义配置
|
// ValidateConfig 校验自定义配置
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package push
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCustomPusherSend_ResponseBodyErrcode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
statusCode int
|
||||||
|
body string
|
||||||
|
wantErr bool
|
||||||
|
wantErrMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "wechat business error returns HTTP 200 with non-zero errcode",
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
body: `{"errcode":93000,"errmsg":"invalid request data"}`,
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "errcode=93000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wechat success returns errcode 0",
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
body: `{"errcode":0,"errmsg":"ok"}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "json response without errcode is tolerated",
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
body: `{"success":true}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-json response body is tolerated",
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
body: "ok",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty response body is tolerated",
|
||||||
|
statusCode: http.StatusNoContent,
|
||||||
|
body: "",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "http error status still fails",
|
||||||
|
statusCode: http.StatusInternalServerError,
|
||||||
|
body: `{"errcode":0,"errmsg":"ok"}`,
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "http status",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(tt.statusCode)
|
||||||
|
_, _ = w.Write([]byte(tt.body))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
pusher := &CustomPusher{}
|
||||||
|
upstreamResp, err := pusher.Send(context.Background(),
|
||||||
|
Config{Channel: "custom", URL: srv.URL},
|
||||||
|
"",
|
||||||
|
map[string]any{"title": "t", "content": "c"},
|
||||||
|
`{"title":"$title","content":"$content"}`,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.wantErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if tt.body != "" {
|
||||||
|
assert.Contains(t, upstreamResp, tt.body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-6
@@ -28,12 +28,12 @@ func sanitizeEmailHeader(v string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send 发送邮件
|
// Send 发送邮件
|
||||||
func (p *EmailPusher) Send(ctx context.Context, cfg Config, target string, body map[string]any, _ string, ext map[string]any) error {
|
func (p *EmailPusher) Send(ctx context.Context, cfg Config, target string, body map[string]any, _ string, ext map[string]any) (string, error) {
|
||||||
if cfg.URL == "" || cfg.Key == "" || cfg.Secret == "" {
|
if cfg.URL == "" || cfg.Key == "" || cfg.Secret == "" {
|
||||||
return errors.New("email: SMTP configuration (url, key, secret) is incomplete")
|
return "", errors.New("email: SMTP configuration (url, key, secret) is incomplete")
|
||||||
}
|
}
|
||||||
if target == "" {
|
if target == "" {
|
||||||
return errors.New("email: target email address is required")
|
return "", errors.New("email: target email address is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
title := defaultTitle
|
title := defaultTitle
|
||||||
@@ -92,14 +92,14 @@ func (p *EmailPusher) Send(ctx context.Context, cfg Config, target string, body
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return "", ctx.Err()
|
||||||
case err := <-errChan:
|
case err := <-errChan:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("email: send smtp mail failed: %w", err)
|
return "", fmt.Errorf("email: send smtp mail failed: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateConfig 校验邮件 SMTP 配置
|
// ValidateConfig 校验邮件 SMTP 配置
|
||||||
|
|||||||
+10
-10
@@ -76,9 +76,9 @@ type larkMessageResponse struct {
|
|||||||
// Send 执行飞书消息发送
|
// Send 执行飞书消息发送
|
||||||
//
|
//
|
||||||
//nolint:nestif,cyclop
|
//nolint:nestif,cyclop
|
||||||
func (p *LarkPusher) Send(ctx context.Context, cfg Config, _ string, body map[string]any, template string, _ map[string]any) error {
|
func (p *LarkPusher) Send(ctx context.Context, cfg Config, _ string, body map[string]any, template string, _ map[string]any) (string, error) {
|
||||||
if cfg.URL == "" {
|
if cfg.URL == "" {
|
||||||
return errors.New("lark: URL is required")
|
return "", errors.New("lark: URL is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
var req larkMessageRequest
|
var req larkMessageRequest
|
||||||
@@ -212,7 +212,7 @@ func (p *LarkPusher) Send(ctx context.Context, cfg Config, _ string, body map[st
|
|||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
sign, err := larkSign(cfg.Secret, timestamp)
|
sign, err := larkSign(cfg.Secret, timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("lark: sign failed: %w", err)
|
return "", fmt.Errorf("lark: sign failed: %w", err)
|
||||||
}
|
}
|
||||||
req.Timestamp = strconv.FormatInt(timestamp, 10)
|
req.Timestamp = strconv.FormatInt(timestamp, 10)
|
||||||
req.Sign = sign
|
req.Sign = sign
|
||||||
@@ -220,37 +220,37 @@ func (p *LarkPusher) Send(ctx context.Context, cfg Config, _ string, body map[st
|
|||||||
|
|
||||||
jsonData, err := json.Marshal(req)
|
jsonData, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("lark: marshal request failed: %w", err)
|
return "", fmt.Errorf("lark: marshal request failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 发送 POST 请求
|
// 4. 发送 POST 请求
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.URL, bytes.NewBuffer(jsonData))
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, cfg.URL, bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("lark: create http request failed: %w", err)
|
return "", fmt.Errorf("lark: create http request failed: %w", err)
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
client := httppool.NewClient(defaultHTTPClientTimeout)
|
client := httppool.NewClient(defaultHTTPClientTimeout)
|
||||||
resp, err := client.Do(httpReq)
|
resp, err := client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("lark: http request failed: %w", err)
|
return "", fmt.Errorf("lark: http request failed: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("lark: http status %s", resp.Status)
|
return "", fmt.Errorf("lark: http status %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
var res larkMessageResponse
|
var res larkMessageResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||||
return fmt.Errorf("lark: decode response failed: %w", err)
|
return "", fmt.Errorf("lark: decode response failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.Code != 0 {
|
if res.Code != 0 {
|
||||||
return fmt.Errorf("lark: send message failed, code %d: %s", res.Code, res.Msg)
|
return "", fmt.Errorf("lark: send message failed, code %d: %s", res.Code, res.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateConfig 校验飞书配置
|
// ValidateConfig 校验飞书配置
|
||||||
|
|||||||
+2
-1
@@ -33,7 +33,8 @@ type Pusher interface {
|
|||||||
// body: 消息体数据 (含默认字段如 title, content, level)
|
// body: 消息体数据 (含默认字段如 title, content, level)
|
||||||
// template: 消息卡片/模板 JSON (可选)
|
// template: 消息卡片/模板 JSON (可选)
|
||||||
// ext: 预留的单次发送拓展数据
|
// ext: 预留的单次发送拓展数据
|
||||||
Send(ctx context.Context, cfg Config, target string, body map[string]any, template string, ext map[string]any) error
|
// 返回 upstreamResp: 上游服务返回的响应内容(如 Webhook 响应体),用于任务日志审计;无响应时为空字符串
|
||||||
|
Send(ctx context.Context, cfg Config, target string, body map[string]any, template string, ext map[string]any) (upstreamResp string, err error)
|
||||||
|
|
||||||
// ValidateConfig 校验渠道配置合法性
|
// ValidateConfig 校验渠道配置合法性
|
||||||
ValidateConfig(cfg Config) error
|
ValidateConfig(cfg Config) error
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ type telegramErrorResponse struct {
|
|||||||
// Send 执行 Telegram 消息发送
|
// Send 执行 Telegram 消息发送
|
||||||
//
|
//
|
||||||
//nolint:cyclop
|
//nolint:cyclop
|
||||||
func (p *TelegramPusher) Send(ctx context.Context, cfg Config, target string, body map[string]any, template string, _ map[string]any) error {
|
func (p *TelegramPusher) Send(ctx context.Context, cfg Config, target string, body map[string]any, template string, _ map[string]any) (string, error) {
|
||||||
if cfg.Secret == "" {
|
if cfg.Secret == "" {
|
||||||
return errors.New("telegram: Bot Token (Secret) is required")
|
return "", errors.New("telegram: Bot Token (Secret) is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
chatID := target
|
chatID := target
|
||||||
@@ -47,7 +47,7 @@ func (p *TelegramPusher) Send(ctx context.Context, cfg Config, target string, bo
|
|||||||
chatID = cfg.Key // Use default chat ID (Key) if target is blank
|
chatID = cfg.Key // Use default chat ID (Key) if target is blank
|
||||||
}
|
}
|
||||||
if chatID == "" {
|
if chatID == "" {
|
||||||
return errors.New("telegram: chat_id (target or default Key) is required")
|
return "", errors.New("telegram: chat_id (target or default Key) is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL := cfg.URL
|
baseURL := cfg.URL
|
||||||
@@ -92,11 +92,11 @@ func (p *TelegramPusher) Send(ctx context.Context, cfg Config, target string, bo
|
|||||||
}
|
}
|
||||||
fallbackErr := p.sendMessage(ctx, baseURL, cfg.Secret, chatID, plainText, "")
|
fallbackErr := p.sendMessage(ctx, baseURL, cfg.Secret, chatID, plainText, "")
|
||||||
if fallbackErr != nil {
|
if fallbackErr != nil {
|
||||||
return fmt.Errorf("telegram: send message failed (fallback also failed): %w (original HTML error: %v)", fallbackErr, err)
|
return "", fmt.Errorf("telegram: send message failed (fallback also failed): %w (original HTML error: %v)", fallbackErr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateConfig 校验 Telegram 配置
|
// ValidateConfig 校验 Telegram 配置
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func TestTelegramPusher_Send(t *testing.T) {
|
|||||||
"content": "Host down",
|
"content": "Host down",
|
||||||
"level": "CRITICAL",
|
"level": "CRITICAL",
|
||||||
}
|
}
|
||||||
err := pusher.Send(context.Background(), cfg, "123456", body, "", nil)
|
_, err := pusher.Send(context.Background(), cfg, "123456", body, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "123456", receivedReq.ChatID)
|
assert.Equal(t, "123456", receivedReq.ChatID)
|
||||||
@@ -79,7 +79,7 @@ func TestTelegramPusher_Send(t *testing.T) {
|
|||||||
"content": "A < B comparison",
|
"content": "A < B comparison",
|
||||||
"level": "INFO",
|
"level": "INFO",
|
||||||
}
|
}
|
||||||
err := pusher.Send(context.Background(), cfg, "123456", body, "", nil)
|
_, err := pusher.Send(context.Background(), cfg, "123456", body, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, requests, 2)
|
require.Len(t, requests, 2)
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
|
func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
|
||||||
@@ -21,16 +21,15 @@ func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 Resource
|
// 业务属性不绑定 schema URL,合并时继承 resource.Default() 的 SDK 内置版本,避免 semconv 与 otel/sdk 升级不同步。
|
||||||
r, err := resource.Merge(
|
r, err := resource.Merge(
|
||||||
resource.Default(),
|
resource.Default(),
|
||||||
resource.NewWithAttributes(
|
resource.NewSchemaless(
|
||||||
semconv.SchemaURL,
|
attribute.String("service.name", cfg.AppName),
|
||||||
semconv.ServiceName(cfg.AppName),
|
attribute.String("host.name", hostname),
|
||||||
semconv.HostName(hostname),
|
attribute.String("k8s.namespace.name", os.Getenv("KUBERNETES_NAMESPACE")),
|
||||||
semconv.K8SNamespaceName(os.Getenv("KUBERNETES_NAMESPACE")),
|
attribute.String("k8s.pod.name", os.Getenv("KUBERNETES_POD_NAME")),
|
||||||
semconv.K8SPodName(os.Getenv("KUBERNETES_POD_NAME")),
|
attribute.String("k8s.pod.uid", os.Getenv("KUBERNETES_POD_UID")),
|
||||||
semconv.K8SPodUID(os.Getenv("KUBERNETES_POD_UID")),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user