This commit is contained in:
ryan
2026-06-18 16:51:06 +08:00
parent 61cfacba55
commit e3bfd9ca6d
40 changed files with 2819 additions and 116 deletions
@@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/relay"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
@@ -143,6 +144,8 @@ func Heartbeat(ctx context.Context, node *model.OpenFlareNode, payload Heartbeat
if err := db.DB(ctx).Model(node).Updates(changes).Error; err != nil {
return nil, fmt.Errorf("update flared heartbeat: %w", err)
}
agent.RefreshAccessTokenCache(ctx, node)
persistFlaredObservability(ctx, node.NodeID, payload, now)
activeConfig, err := getActiveConfigMeta(ctx)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -0,0 +1,50 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package flared
import (
"context"
"fmt"
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
"github.com/Rain-kl/Wavelet/internal/db"
"go.uber.org/zap"
)
const flaredRuntimeUnhealthyEventType = "flared_runtime_unhealthy"
func persistFlaredObservability(ctx context.Context, nodeID string, payload HeartbeatPayload, reportedAt time.Time) {
connected := make([]string, 0, len(payload.ConnectedRelays))
for _, relay := range payload.ConnectedRelays {
connected = append(connected, fmt.Sprintf("%s:%s", relay.RelayNodeID, relay.Status))
}
managedTypes := map[string]struct{}{
flaredRuntimeUnhealthyEventType: {},
}
var events []agent.NodeHealthEvent
if payload.TunnelStatus == "unhealthy" {
events = append(events, agent.NodeHealthEvent{
EventType: flaredRuntimeUnhealthyEventType,
Severity: "critical",
Message: "openflared runtime is not healthy",
TriggeredAtUnix: reportedAt.Unix(),
Metadata: map[string]string{
"tunnel_status": payload.TunnelStatus,
"client_version": payload.ClientVersion,
"current_version": payload.CurrentVersion,
"current_checksum": payload.CurrentChecksum,
"connected_relays": strings.Join(connected, ","),
},
})
}
conn := db.DB(ctx)
if conn == nil {
return
}
if err := agent.ReconcileScopedNodeHealthEvents(conn, nodeID, events, reportedAt, managedTypes); err != nil {
zap.L().Error("persist flared health events failed", zap.String("node_id", nodeID), zap.Error(err))
}
}
@@ -0,0 +1,71 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package flared
import (
"context"
"testing"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/option"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func setupFlaredObservabilityTestDB(t *testing.T) func() {
t.Helper()
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
require.NoError(t, err)
require.NoError(t, sqliteDB.AutoMigrate(
&model.OpenFlareNode{},
&model.OpenFlareHealthEvent{},
))
db.SetDB(sqliteDB)
option.ResetInitializationForTest()
agent.ResetAuthCacheForTest()
return func() {
db.SetDB(nil)
option.ResetInitializationForTest()
agent.ResetAuthCacheForTest()
}
}
func TestHeartbeatFlaredEmitsHealthEventOnUnhealthy(t *testing.T) {
cleanup := setupFlaredObservabilityTestDB(t)
defer cleanup()
ctx := context.Background()
node := &model.OpenFlareNode{
NodeID: "node-flared-unhealthy",
Name: "flared-unhealthy",
AccessToken: "tunnel-token-unhealthy",
Status: "pending",
NodeType: "tunnel_client",
}
require.NoError(t, db.DB(ctx).Create(node).Error)
_, err := Heartbeat(ctx, node, HeartbeatPayload{
ClientVersion: "v0.2.0",
FrpVersion: "0.61.0",
TunnelStatus: "unhealthy",
CurrentVersion: "v1",
CurrentChecksum: "checksum-1",
})
require.NoError(t, err)
events, err := model.ListOpenFlareHealthEvents(ctx, node.NodeID, false, 20)
require.NoError(t, err)
require.Len(t, events, 1)
assert.Equal(t, flaredRuntimeUnhealthyEventType, events[0].EventType)
assert.Equal(t, "active", events[0].Status)
}