mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 14:46:36 +08:00
299 lines
7.3 KiB
Go
299 lines
7.3 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package analytics
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/db"
|
|
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
|
analyticsmodel "github.com/Rain-kl/Wavelet/internal/model/analytics"
|
|
)
|
|
|
|
// InsertNodeMetricSnapshot writes a metric snapshot when no row exists for node_id+captured_at.
|
|
func InsertNodeMetricSnapshot(ctx context.Context, snapshot analyticsmodel.NodeMetricSnapshot) error {
|
|
nodeID := strings.TrimSpace(snapshot.NodeID)
|
|
if nodeID == "" {
|
|
return nil
|
|
}
|
|
capturedAt := snapshot.CapturedAt.UTC()
|
|
|
|
exists, err := nodeObservabilityRowExists(
|
|
ctx,
|
|
fmt.Sprintf("SELECT count() FROM %s WHERE node_id = ? AND captured_at = ?", nodeMetricSnapshotTableName()),
|
|
nodeID, capturedAt,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
return nil
|
|
}
|
|
|
|
if db.ChConn == nil {
|
|
return fmt.Errorf("clickhouse connection is not initialized")
|
|
}
|
|
|
|
batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeMetricSnapshot{}.BatchInsertSQL())
|
|
if err != nil {
|
|
return fmt.Errorf("prepare clickhouse batch: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
id := snapshot.ID
|
|
if id == 0 {
|
|
id = idgen.NextUint64ID()
|
|
}
|
|
createdAt := snapshot.CreatedAt
|
|
if createdAt.IsZero() {
|
|
createdAt = now
|
|
}
|
|
if err := batch.Append(
|
|
id,
|
|
nodeID,
|
|
capturedAt,
|
|
snapshot.CPUUsagePercent,
|
|
snapshot.MemoryUsedBytes,
|
|
snapshot.MemoryTotalBytes,
|
|
snapshot.StorageUsedBytes,
|
|
snapshot.StorageTotalBytes,
|
|
snapshot.DiskReadBytes,
|
|
snapshot.DiskWriteBytes,
|
|
snapshot.NetworkRxBytes,
|
|
snapshot.NetworkTxBytes,
|
|
createdAt.UTC(),
|
|
); err != nil {
|
|
return fmt.Errorf("append node metric snapshot to batch: %w", err)
|
|
}
|
|
if err := batch.Send(); err != nil {
|
|
return fmt.Errorf("send clickhouse batch: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InsertNodeRequestReport writes a request report when no row exists for node_id+window bounds.
|
|
func InsertNodeRequestReport(ctx context.Context, report analyticsmodel.NodeRequestReport) error {
|
|
nodeID := strings.TrimSpace(report.NodeID)
|
|
if nodeID == "" {
|
|
return nil
|
|
}
|
|
windowStartedAt := report.WindowStartedAt.UTC()
|
|
windowEndedAt := report.WindowEndedAt.UTC()
|
|
|
|
exists, err := nodeObservabilityRowExists(
|
|
ctx,
|
|
fmt.Sprintf(
|
|
"SELECT count() FROM %s WHERE node_id = ? AND window_started_at = ? AND window_ended_at = ?",
|
|
nodeRequestReportTableName(),
|
|
),
|
|
nodeID, windowStartedAt, windowEndedAt,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
return nil
|
|
}
|
|
|
|
if db.ChConn == nil {
|
|
return fmt.Errorf("clickhouse connection is not initialized")
|
|
}
|
|
|
|
batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeRequestReport{}.BatchInsertSQL())
|
|
if err != nil {
|
|
return fmt.Errorf("prepare clickhouse batch: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
id := report.ID
|
|
if id == 0 {
|
|
id = idgen.NextUint64ID()
|
|
}
|
|
createdAt := report.CreatedAt
|
|
if createdAt.IsZero() {
|
|
createdAt = now
|
|
}
|
|
if err := batch.Append(
|
|
id,
|
|
nodeID,
|
|
windowStartedAt,
|
|
windowEndedAt,
|
|
report.RequestCount,
|
|
report.ErrorCount,
|
|
report.UniqueVisitorCount,
|
|
report.StatusCodesJSON,
|
|
report.TopDomainsJSON,
|
|
report.SourceCountriesJSON,
|
|
createdAt.UTC(),
|
|
); err != nil {
|
|
return fmt.Errorf("append node request report to batch: %w", err)
|
|
}
|
|
if err := batch.Send(); err != nil {
|
|
return fmt.Errorf("send clickhouse batch: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// InsertNodeObsOpenresty writes an OpenResty observability snapshot.
|
|
func InsertNodeObsOpenresty(ctx context.Context, obs analyticsmodel.NodeObsOpenresty) error {
|
|
nodeID := strings.TrimSpace(obs.NodeID)
|
|
if nodeID == "" {
|
|
return nil
|
|
}
|
|
return insertNodeObsOpenrestyBatch(ctx, obs, nodeID)
|
|
}
|
|
|
|
// InsertNodeObsFrps writes an FRPS observability snapshot.
|
|
func InsertNodeObsFrps(ctx context.Context, obs analyticsmodel.NodeObsFrps) error {
|
|
nodeID := strings.TrimSpace(obs.NodeID)
|
|
if nodeID == "" {
|
|
return nil
|
|
}
|
|
return insertNodeObsFrpsBatch(ctx, obs, nodeID)
|
|
}
|
|
|
|
// InsertNodeObsFrpc writes an FRPC observability snapshot.
|
|
func InsertNodeObsFrpc(ctx context.Context, obs analyticsmodel.NodeObsFrpc) error {
|
|
nodeID := strings.TrimSpace(obs.NodeID)
|
|
if nodeID == "" {
|
|
return nil
|
|
}
|
|
return insertNodeObsFrpcBatch(ctx, obs, nodeID)
|
|
}
|
|
|
|
func insertNodeObsOpenrestyBatch(ctx context.Context, obs analyticsmodel.NodeObsOpenresty, nodeID string) error {
|
|
if db.ChConn == nil {
|
|
return fmt.Errorf("clickhouse connection is not initialized")
|
|
}
|
|
|
|
batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeObsOpenresty{}.BatchInsertSQL())
|
|
if err != nil {
|
|
return fmt.Errorf("prepare clickhouse batch: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
id := obs.ID
|
|
if id == 0 {
|
|
id = idgen.NextUint64ID()
|
|
}
|
|
createdAt := obs.CreatedAt
|
|
if createdAt.IsZero() {
|
|
createdAt = now
|
|
}
|
|
capturedAt := obs.CapturedAt.UTC()
|
|
if capturedAt.IsZero() {
|
|
capturedAt = now
|
|
}
|
|
if err := batch.Append(
|
|
id,
|
|
nodeID,
|
|
capturedAt,
|
|
obs.OpenrestyRxBytes,
|
|
obs.OpenrestyTxBytes,
|
|
obs.OpenrestyConnections,
|
|
createdAt.UTC(),
|
|
); err != nil {
|
|
return fmt.Errorf("append node openresty observation to batch: %w", err)
|
|
}
|
|
if err := batch.Send(); err != nil {
|
|
return fmt.Errorf("send clickhouse batch: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func insertNodeObsFrpsBatch(ctx context.Context, obs analyticsmodel.NodeObsFrps, nodeID string) error {
|
|
if db.ChConn == nil {
|
|
return fmt.Errorf("clickhouse connection is not initialized")
|
|
}
|
|
|
|
batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeObsFrps{}.BatchInsertSQL())
|
|
if err != nil {
|
|
return fmt.Errorf("prepare clickhouse batch: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
id := obs.ID
|
|
if id == 0 {
|
|
id = idgen.NextUint64ID()
|
|
}
|
|
createdAt := obs.CreatedAt
|
|
if createdAt.IsZero() {
|
|
createdAt = now
|
|
}
|
|
capturedAt := obs.CapturedAt.UTC()
|
|
if capturedAt.IsZero() {
|
|
capturedAt = now
|
|
}
|
|
if err := batch.Append(
|
|
id,
|
|
nodeID,
|
|
capturedAt,
|
|
obs.FrpsConnections,
|
|
obs.FrpsProxyCount,
|
|
obs.FrpsClientCount,
|
|
obs.FrpsProxies,
|
|
createdAt.UTC(),
|
|
); err != nil {
|
|
return fmt.Errorf("append node frps observation to batch: %w", err)
|
|
}
|
|
if err := batch.Send(); err != nil {
|
|
return fmt.Errorf("send clickhouse batch: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func insertNodeObsFrpcBatch(ctx context.Context, obs analyticsmodel.NodeObsFrpc, nodeID string) error {
|
|
if db.ChConn == nil {
|
|
return fmt.Errorf("clickhouse connection is not initialized")
|
|
}
|
|
|
|
batch, err := db.ChConn.PrepareBatch(ctx, analyticsmodel.NodeObsFrpc{}.BatchInsertSQL())
|
|
if err != nil {
|
|
return fmt.Errorf("prepare clickhouse batch: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
id := obs.ID
|
|
if id == 0 {
|
|
id = idgen.NextUint64ID()
|
|
}
|
|
createdAt := obs.CreatedAt
|
|
if createdAt.IsZero() {
|
|
createdAt = now
|
|
}
|
|
capturedAt := obs.CapturedAt.UTC()
|
|
if capturedAt.IsZero() {
|
|
capturedAt = now
|
|
}
|
|
if err := batch.Append(
|
|
id,
|
|
nodeID,
|
|
capturedAt,
|
|
obs.TunnelStatus,
|
|
obs.ConnectedRelaysCount,
|
|
createdAt.UTC(),
|
|
); err != nil {
|
|
return fmt.Errorf("append node frpc observation to batch: %w", err)
|
|
}
|
|
if err := batch.Send(); err != nil {
|
|
return fmt.Errorf("send clickhouse batch: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nodeObservabilityRowExists(ctx context.Context, countSQL string, args ...any) (bool, error) {
|
|
conn, err := observabilityConn()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
var count int64
|
|
if err := conn.QueryRow(ctx, countSQL, args...).Scan(&count); err != nil {
|
|
return false, fmt.Errorf("check observability row exists: %w", err)
|
|
}
|
|
return count > 0, nil
|
|
}
|