feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
211
go/vehicle-gateway/cmd/feichi-bridge/main.go
Normal file
211
go/vehicle-gateway/cmd/feichi-bridge/main.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/feichibridge"
|
||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/health"
|
||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics"
|
||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/observability"
|
||||
)
|
||||
|
||||
type authSecret struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type targetSecret struct {
|
||||
PlatformID string `json:"platformId"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
BaseURL string
|
||||
AuthSecretFile string
|
||||
TargetSecret string
|
||||
TargetAddress string
|
||||
StateFile string
|
||||
HealthAddress string
|
||||
HTTPTimeout time.Duration
|
||||
TargetTimeout time.Duration
|
||||
OCRImage string
|
||||
OCRTimeout time.Duration
|
||||
LoginAttempts int
|
||||
LoginRetryDelay time.Duration
|
||||
Service feichibridge.ServiceConfig
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger := observability.NewLogger("feichi-bridge")
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
logger.Error("load configuration failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
auth, err := readJSONSecret[authSecret](cfg.AuthSecretFile)
|
||||
if err != nil {
|
||||
logger.Error("read Feichi API secret failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
credentials, err := readJSONSecret[targetSecret](cfg.TargetSecret)
|
||||
if err != nil {
|
||||
logger.Error("read GB/T 32960 target secret failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
source, err := feichibridge.NewAuthenticatedAPIClient(
|
||||
cfg.BaseURL,
|
||||
feichibridge.LoginCredentials{
|
||||
Username: auth.Username, Password: auth.Password,
|
||||
MaxAttempts: cfg.LoginAttempts, RetryDelay: cfg.LoginRetryDelay,
|
||||
},
|
||||
feichibridge.DockerCaptchaSolver{Image: cfg.OCRImage, Timeout: cfg.OCRTimeout},
|
||||
cfg.HTTPTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("build Feichi API client failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
state, err := feichibridge.OpenStateStore(cfg.StateFile)
|
||||
if err != nil {
|
||||
logger.Error("open bridge state failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
target, err := feichibridge.NewTarget(feichibridge.TargetConfig{
|
||||
Address: cfg.TargetAddress, PlatformID: credentials.PlatformID,
|
||||
Username: credentials.Username, Password: credentials.Password,
|
||||
Timeout: cfg.TargetTimeout,
|
||||
}, state)
|
||||
if err != nil {
|
||||
logger.Error("build GB/T 32960 target failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
registry := metrics.NewRegistry()
|
||||
service, err := feichibridge.NewService(cfg.Service, source, target, state, logger, registry)
|
||||
if err != nil {
|
||||
logger.Error("build bridge service failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
health.Start(ctx, logger, health.NewServer(cfg.HealthAddress, "feichi-bridge", []health.Check{
|
||||
{Name: "bridge", Check: service.Ready},
|
||||
}, registry))
|
||||
logger.Info("Feichi bridge starting",
|
||||
"base_url", cfg.BaseURL,
|
||||
"target_address", cfg.TargetAddress,
|
||||
"poll_interval", cfg.Service.PollInterval,
|
||||
"backfill_enabled", cfg.Service.BackfillEnabled,
|
||||
)
|
||||
if strings.HasPrefix(strings.ToLower(cfg.BaseURL), "http://") {
|
||||
logger.Warn("Feichi source uses clear-text HTTP; deploy only through a controlled egress path")
|
||||
}
|
||||
if err := service.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
logger.Error("Feichi bridge stopped unexpectedly", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Info("Feichi bridge stopped")
|
||||
}
|
||||
|
||||
func loadConfig() (config, error) {
|
||||
cfg := config{
|
||||
BaseURL: strings.TrimSpace(os.Getenv("FEICHI_BASE_URL")),
|
||||
AuthSecretFile: env("FEICHI_AUTH_SECRET_FILE", strings.TrimSpace(os.Getenv("FEICHI_AUTH_HEADERS_FILE"))),
|
||||
TargetSecret: strings.TrimSpace(os.Getenv("FEICHI_TARGET_SECRET_FILE")),
|
||||
TargetAddress: env("FEICHI_TARGET_ADDR", "127.0.0.1:32960"),
|
||||
StateFile: env("FEICHI_STATE_FILE", "/var/lib/lingniu-feichi-bridge/state.json"),
|
||||
HealthAddress: env("HEALTH_ADDR", "127.0.0.1:20219"),
|
||||
HTTPTimeout: seconds("FEICHI_HTTP_TIMEOUT_SECONDS", 15),
|
||||
TargetTimeout: seconds("FEICHI_TARGET_TIMEOUT_SECONDS", 10),
|
||||
OCRImage: env("FEICHI_OCR_IMAGE", "lingniu/feichi-captcha-ocr:1.0.0"),
|
||||
OCRTimeout: seconds("FEICHI_OCR_TIMEOUT_SECONDS", 20),
|
||||
LoginAttempts: envInt("FEICHI_LOGIN_MAX_ATTEMPTS", 20),
|
||||
LoginRetryDelay: seconds("FEICHI_LOGIN_RETRY_SECONDS", 1),
|
||||
Service: feichibridge.ServiceConfig{
|
||||
PollInterval: seconds("FEICHI_POLL_INTERVAL_SECONDS", 10),
|
||||
DiscoveryInterval: seconds("FEICHI_DISCOVERY_INTERVAL_SECONDS", 300),
|
||||
BackfillInterval: seconds("FEICHI_BACKFILL_INTERVAL_SECONDS", 3600),
|
||||
BackfillLookback: seconds("FEICHI_BACKFILL_LOOKBACK_SECONDS", 3600),
|
||||
BackfillWindow: seconds("FEICHI_BACKFILL_WINDOW_SECONDS", 1200),
|
||||
BackfillSafetyLag: seconds("FEICHI_BACKFILL_SAFETY_SECONDS", 30),
|
||||
SourceStaleAfter: seconds("FEICHI_SOURCE_STALE_SECONDS", 120),
|
||||
FetchConcurrency: envInt("FEICHI_FETCH_CONCURRENCY", 4),
|
||||
BackfillEnabled: envBool("FEICHI_BACKFILL_ENABLED", true),
|
||||
StaleReissueEnabled: envBool("FEICHI_STALE_REISSUE_ENABLED", true),
|
||||
},
|
||||
}
|
||||
var missing []string
|
||||
if cfg.BaseURL == "" {
|
||||
missing = append(missing, "FEICHI_BASE_URL")
|
||||
}
|
||||
if cfg.AuthSecretFile == "" {
|
||||
missing = append(missing, "FEICHI_AUTH_SECRET_FILE")
|
||||
}
|
||||
if cfg.TargetSecret == "" {
|
||||
missing = append(missing, "FEICHI_TARGET_SECRET_FILE")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return config{}, fmt.Errorf("required configuration missing: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
if cfg.Service.BackfillWindow > 24*time.Hour {
|
||||
return config{}, errors.New("FEICHI_BACKFILL_WINDOW_SECONDS must not exceed 86400")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func readJSONSecret[T any](path string) (T, error) {
|
||||
var value T
|
||||
encoded, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
if err := json.Unmarshal(encoded, &value); err != nil {
|
||||
return value, fmt.Errorf("decode %s: %w", path, err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func env(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envInt(name string, fallback int) int {
|
||||
value := strings.TrimSpace(os.Getenv(name))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil || parsed <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func envBool(name string, fallback bool) bool {
|
||||
value := strings.TrimSpace(os.Getenv(name))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func seconds(name string, fallback int) time.Duration {
|
||||
return time.Duration(envInt(name, fallback)) * time.Second
|
||||
}
|
||||
Reference in New Issue
Block a user