2024-07-19 20:02:18 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-18 14:15:58 +08:00
|
|
|
"caatsm/internal/infra/buildinfo"
|
2025-11-14 22:43:04 +08:00
|
|
|
"caatsm/internal/infra/config"
|
2025-11-14 08:42:26 +08:00
|
|
|
"caatsm/pkg/di"
|
|
|
|
|
"context"
|
2025-11-14 21:42:04 +08:00
|
|
|
"errors"
|
2024-07-22 12:42:30 +08:00
|
|
|
"fmt"
|
2025-11-14 08:42:26 +08:00
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
2025-11-15 09:01:24 +08:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2025-11-14 08:42:26 +08:00
|
|
|
"syscall"
|
2025-11-14 21:42:04 +08:00
|
|
|
"time"
|
2024-08-05 10:11:33 +08:00
|
|
|
|
2025-11-17 16:25:23 +08:00
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
2024-08-05 10:11:33 +08:00
|
|
|
"github.com/urfave/cli/v2"
|
2025-11-15 09:01:24 +08:00
|
|
|
"go.opentelemetry.io/otel"
|
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
|
|
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
|
|
|
"go.opentelemetry.io/otel/propagation"
|
|
|
|
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
|
|
|
"go.opentelemetry.io/otel/sdk/resource"
|
|
|
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
|
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
2024-07-19 20:02:18 +08:00
|
|
|
)
|
|
|
|
|
|
2025-11-16 13:14:46 +08:00
|
|
|
// main is the entrypoint for the caatsm CLI.
|
|
|
|
|
// It delegates all logic to run so that startup behaviour can be tested.
|
2024-07-19 20:02:18 +08:00
|
|
|
func main() {
|
2025-11-16 13:14:46 +08:00
|
|
|
if err := run(os.Args); err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "caatsm failed: %v\n", err)
|
2025-11-14 08:42:26 +08:00
|
|
|
os.Exit(1)
|
2024-08-05 10:11:33 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 13:14:46 +08:00
|
|
|
// run constructs the CLI application and executes it with the provided args.
|
|
|
|
|
func run(args []string) error {
|
|
|
|
|
app := setupApp()
|
|
|
|
|
return app.Run(args)
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-05 10:11:33 +08:00
|
|
|
func setupApp() *cli.App {
|
2025-11-14 08:42:26 +08:00
|
|
|
return &cli.App{
|
2024-08-05 10:58:20 +08:00
|
|
|
Name: "telegram message process",
|
2025-11-14 08:42:26 +08:00
|
|
|
Usage: "A Civil Aviation Authority Telegram Message Processor",
|
2024-08-05 10:11:33 +08:00
|
|
|
Commands: []*cli.Command{
|
|
|
|
|
{
|
2024-08-05 10:58:20 +08:00
|
|
|
Name: "listen",
|
|
|
|
|
Usage: "Listen to nats messages",
|
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
|
&cli.StringFlag{
|
2025-11-15 09:01:24 +08:00
|
|
|
Name: "nats-url",
|
2024-08-05 11:42:33 +08:00
|
|
|
Aliases: []string{"n"},
|
2025-11-15 09:01:24 +08:00
|
|
|
Usage: "NATS server address",
|
2024-08-05 10:58:20 +08:00
|
|
|
EnvVars: []string{"NATS_SERVER"},
|
|
|
|
|
},
|
|
|
|
|
&cli.StringFlag{
|
2025-11-15 09:01:24 +08:00
|
|
|
Name: "subject",
|
2024-08-05 11:42:33 +08:00
|
|
|
Aliases: []string{"t"},
|
2025-11-15 09:01:24 +08:00
|
|
|
Usage: "NATS subject to listen to",
|
2024-08-05 10:58:20 +08:00
|
|
|
EnvVars: []string{"NATS_SUBJECT"},
|
|
|
|
|
},
|
2025-11-15 09:01:24 +08:00
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "stream",
|
|
|
|
|
Usage: "NATS JetStream stream name",
|
|
|
|
|
},
|
2025-11-15 12:08:26 +08:00
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "nats-mode",
|
|
|
|
|
Usage: "NATS mode: jetstream or core",
|
|
|
|
|
},
|
2025-11-15 09:01:24 +08:00
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "consumer",
|
|
|
|
|
Usage: "NATS JetStream durable consumer",
|
|
|
|
|
},
|
|
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "publisher-topic",
|
|
|
|
|
Usage: "Subject used by the publisher",
|
|
|
|
|
},
|
|
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "postgres-url",
|
|
|
|
|
Usage: "PostgreSQL connection URL",
|
|
|
|
|
EnvVars: []string{
|
|
|
|
|
"POSTGRES_URL",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "log-level",
|
|
|
|
|
Usage: "Logger level (debug, info, warn, error)",
|
|
|
|
|
},
|
|
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "replay-from",
|
|
|
|
|
Usage: "Override deliver policy: all|new|last|seq:<n>|time:<RFC3339>",
|
|
|
|
|
},
|
|
|
|
|
&cli.DurationFlag{
|
|
|
|
|
Name: "ack-wait",
|
|
|
|
|
Usage: "Override consumer ack wait duration",
|
|
|
|
|
},
|
|
|
|
|
&cli.BoolFlag{
|
|
|
|
|
Name: "telemetry-enabled",
|
|
|
|
|
Usage: "Enable OpenTelemetry exporters",
|
|
|
|
|
},
|
|
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "telemetry-endpoint",
|
|
|
|
|
Usage: "OpenTelemetry collector OTLP endpoint",
|
|
|
|
|
},
|
|
|
|
|
&cli.BoolFlag{
|
|
|
|
|
Name: "telemetry-insecure",
|
|
|
|
|
Usage: "Send OTLP data without TLS",
|
|
|
|
|
},
|
2025-11-17 17:53:24 +08:00
|
|
|
&cli.StringFlag{
|
|
|
|
|
Name: "monitoring-addr",
|
|
|
|
|
Usage: "Monitoring server address (e.g., 192.168.1.100:2112 or :2112)",
|
|
|
|
|
EnvVars: []string{"CAATSM_MONITORING_ADDR"},
|
|
|
|
|
},
|
|
|
|
|
&cli.BoolFlag{
|
|
|
|
|
Name: "monitoring-disabled",
|
|
|
|
|
Usage: "Disable monitoring server",
|
|
|
|
|
EnvVars: []string{"CAATSM_MONITORING_DISABLED"},
|
|
|
|
|
},
|
2024-08-05 10:58:20 +08:00
|
|
|
},
|
2024-08-05 10:11:33 +08:00
|
|
|
Action: executeListen,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
2024-08-05 10:58:20 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 13:14:46 +08:00
|
|
|
// executeListen is the CLI handler for the "listen" command.
|
|
|
|
|
// It is responsible for loading configuration, applying CLI overrides,
|
|
|
|
|
// and delegating the main processing lifecycle to runListen.
|
2024-08-05 10:11:33 +08:00
|
|
|
func executeListen(c *cli.Context) error {
|
2025-11-14 22:43:04 +08:00
|
|
|
cfg, err := config.LoadConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
applyCLIOverrides(cfg, c)
|
2025-11-14 22:43:04 +08:00
|
|
|
|
2025-11-16 13:14:46 +08:00
|
|
|
// Re-validate configuration after applying CLI overrides to ensure
|
|
|
|
|
// the resulting configuration is still consistent.
|
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
|
|
|
return fmt.Errorf("invalid configuration after CLI overrides: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return runListen(context.Background(), cfg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runListen coordinates telemetry initialisation, dependency wiring,
|
|
|
|
|
// signal handling and graceful shutdown for the listener workflow.
|
|
|
|
|
func runListen(parentCtx context.Context, cfg *config.Config) error {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return fmt.Errorf("config must not be nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, stop := signal.NotifyContext(parentCtx, os.Interrupt, syscall.SIGTERM)
|
|
|
|
|
defer stop()
|
|
|
|
|
|
2025-11-18 14:15:58 +08:00
|
|
|
var shutdownTelemetry func(context.Context) error
|
2025-11-15 09:01:24 +08:00
|
|
|
if cfg.Telemetry.Enabled {
|
|
|
|
|
var telErr error
|
2025-11-16 13:14:46 +08:00
|
|
|
shutdownTelemetry, telErr = initTelemetry(ctx, cfg)
|
2025-11-15 09:01:24 +08:00
|
|
|
if telErr != nil {
|
|
|
|
|
return fmt.Errorf("failed to initialize telemetry: %w", telErr)
|
|
|
|
|
}
|
2025-11-18 14:15:58 +08:00
|
|
|
defer func() {
|
|
|
|
|
if err := shutdownTelemetry(context.Background()); err != nil {
|
|
|
|
|
zap.L().Error("Failed to shutdown telemetry", zap.Error(err))
|
|
|
|
|
}
|
|
|
|
|
}()
|
2025-11-14 22:43:04 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-14 08:42:26 +08:00
|
|
|
// Initialize dependencies using Wire
|
2025-11-15 16:30:29 +08:00
|
|
|
processor, consumer, monitorServer, err := di.InitializeAppWithConfig(cfg)
|
2024-08-05 11:23:22 +08:00
|
|
|
if err != nil {
|
2025-11-14 08:42:26 +08:00
|
|
|
return fmt.Errorf("failed to initialize app: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 16:30:29 +08:00
|
|
|
if monitorServer != nil {
|
|
|
|
|
if err := monitorServer.Start(ctx); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to start monitoring server: %w", err)
|
|
|
|
|
}
|
2025-11-18 14:15:58 +08:00
|
|
|
defer func() {
|
|
|
|
|
if err := monitorServer.Shutdown(context.Background()); err != nil {
|
|
|
|
|
zap.L().Error("Failed to shutdown monitoring server", zap.Error(err))
|
|
|
|
|
}
|
|
|
|
|
}()
|
2025-11-15 16:30:29 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-14 08:42:26 +08:00
|
|
|
// Start consumer in a goroutine
|
|
|
|
|
errChan := make(chan error, 1)
|
|
|
|
|
go func() {
|
2025-11-16 13:14:46 +08:00
|
|
|
if err := consumer.Start(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
2025-11-14 08:42:26 +08:00
|
|
|
errChan <- fmt.Errorf("consumer error: %w", err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2025-11-14 21:42:04 +08:00
|
|
|
var runErr error
|
|
|
|
|
|
2025-11-16 13:14:46 +08:00
|
|
|
// Wait for shutdown signal or consumer error
|
2025-11-14 08:42:26 +08:00
|
|
|
select {
|
2025-11-16 13:14:46 +08:00
|
|
|
case <-ctx.Done():
|
2025-11-17 16:25:23 +08:00
|
|
|
zap.L().Info("Received shutdown signal, shutting down",
|
|
|
|
|
zap.Error(ctx.Err()))
|
2025-11-14 08:42:26 +08:00
|
|
|
case err := <-errChan:
|
2025-11-16 13:14:46 +08:00
|
|
|
if err != nil {
|
2025-11-14 21:42:04 +08:00
|
|
|
runErr = err
|
|
|
|
|
}
|
2025-11-16 13:14:46 +08:00
|
|
|
// Ensure all downstream users of ctx see cancellation.
|
|
|
|
|
stop()
|
2025-11-14 21:42:04 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 13:14:46 +08:00
|
|
|
// After cancellation, give the consumer a chance to finish cleanup.
|
2025-11-18 11:47:35 +08:00
|
|
|
// Consumer checks context before fetch and between messages, so it should exit quickly.
|
|
|
|
|
// Worst case: finishing current message (up to 1s for slow DB) + cleanup (~100ms)
|
|
|
|
|
// 1.5s provides safe buffer while keeping shutdown responsive.
|
|
|
|
|
waitTimeout := 1500 * time.Millisecond
|
2025-11-14 21:42:04 +08:00
|
|
|
select {
|
|
|
|
|
case err := <-errChan:
|
|
|
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
|
|
|
runErr = err
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(waitTimeout):
|
2025-11-17 16:25:23 +08:00
|
|
|
zap.L().Warn("Timed out waiting for consumer shutdown",
|
|
|
|
|
zap.Duration("timeout", waitTimeout))
|
2025-11-14 21:42:04 +08:00
|
|
|
}
|
|
|
|
|
|
2025-11-18 11:47:35 +08:00
|
|
|
// Use a short timeout for shutdown since consumer should already be stopped
|
|
|
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
|
|
|
defer shutdownCancel()
|
|
|
|
|
if err := consumer.Shutdown(shutdownCtx); err != nil {
|
2025-11-14 21:42:04 +08:00
|
|
|
if runErr == nil {
|
|
|
|
|
runErr = fmt.Errorf("failed to drain NATS connection: %w", err)
|
|
|
|
|
}
|
2024-08-05 11:23:22 +08:00
|
|
|
}
|
2025-11-14 08:42:26 +08:00
|
|
|
|
|
|
|
|
// Note: processor is initialized but not directly used here
|
|
|
|
|
_ = processor
|
|
|
|
|
|
2025-11-14 21:42:04 +08:00
|
|
|
return runErr
|
2024-07-19 20:02:18 +08:00
|
|
|
}
|
2025-11-15 09:01:24 +08:00
|
|
|
|
|
|
|
|
func applyCLIOverrides(cfg *config.Config, c *cli.Context) {
|
|
|
|
|
if cfg == nil || c == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if flagURL := c.String("nats-url"); flagURL != "" {
|
|
|
|
|
cfg.NATS.URL = flagURL
|
|
|
|
|
}
|
|
|
|
|
if flagTopic := c.String("subject"); flagTopic != "" {
|
|
|
|
|
cfg.Subscription.Topic = flagTopic
|
|
|
|
|
}
|
|
|
|
|
if stream := c.String("stream"); stream != "" {
|
|
|
|
|
cfg.NATS.Stream = stream
|
|
|
|
|
}
|
2025-11-15 12:08:26 +08:00
|
|
|
if mode := c.String("nats-mode"); mode != "" {
|
|
|
|
|
cfg.NATS.Mode = strings.ToLower(mode)
|
|
|
|
|
}
|
2025-11-15 09:01:24 +08:00
|
|
|
if consumer := c.String("consumer"); consumer != "" {
|
|
|
|
|
cfg.NATS.Consumer = consumer
|
|
|
|
|
}
|
|
|
|
|
if publisherTopic := c.String("publisher-topic"); publisherTopic != "" {
|
|
|
|
|
cfg.Publisher.Topic = publisherTopic
|
|
|
|
|
}
|
|
|
|
|
if pgURL := c.String("postgres-url"); pgURL != "" {
|
|
|
|
|
cfg.Postgres.URL = pgURL
|
|
|
|
|
}
|
|
|
|
|
if logLevel := c.String("log-level"); logLevel != "" {
|
|
|
|
|
cfg.Log.Level = logLevel
|
|
|
|
|
}
|
|
|
|
|
if replay := c.String("replay-from"); replay != "" {
|
|
|
|
|
applyReplayOverride(cfg, replay)
|
|
|
|
|
}
|
|
|
|
|
if c.IsSet("ack-wait") {
|
|
|
|
|
if ack := c.Duration("ack-wait"); ack > 0 {
|
|
|
|
|
cfg.Timeouts.AckWait = ack
|
|
|
|
|
cfg.NATS.ConsumerRules.AckWait = ack
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if c.IsSet("telemetry-enabled") {
|
|
|
|
|
cfg.Telemetry.Enabled = c.Bool("telemetry-enabled")
|
|
|
|
|
}
|
|
|
|
|
if endpoint := c.String("telemetry-endpoint"); endpoint != "" {
|
|
|
|
|
cfg.Telemetry.Endpoint = endpoint
|
|
|
|
|
}
|
|
|
|
|
if c.IsSet("telemetry-insecure") {
|
|
|
|
|
cfg.Telemetry.Insecure = c.Bool("telemetry-insecure")
|
|
|
|
|
}
|
2025-11-17 17:53:24 +08:00
|
|
|
if addr := c.String("monitoring-addr"); addr != "" {
|
|
|
|
|
cfg.Monitoring.Addr = addr
|
|
|
|
|
cfg.Monitoring.Disabled = false
|
|
|
|
|
}
|
|
|
|
|
if c.IsSet("monitoring-disabled") {
|
|
|
|
|
cfg.Monitoring.Disabled = c.Bool("monitoring-disabled")
|
|
|
|
|
}
|
2025-11-15 09:01:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyReplayOverride(cfg *config.Config, value string) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
lower := strings.ToLower(strings.TrimSpace(value))
|
|
|
|
|
switch {
|
|
|
|
|
case lower == "new":
|
|
|
|
|
cfg.NATS.ConsumerRules.DeliverPolicy = "new"
|
|
|
|
|
cfg.NATS.ConsumerRules.StartSequence = 0
|
|
|
|
|
cfg.NATS.ConsumerRules.StartTime = ""
|
|
|
|
|
case lower == "all":
|
|
|
|
|
cfg.NATS.ConsumerRules.DeliverPolicy = "all"
|
|
|
|
|
cfg.NATS.ConsumerRules.StartSequence = 0
|
|
|
|
|
cfg.NATS.ConsumerRules.StartTime = ""
|
|
|
|
|
case lower == "last":
|
|
|
|
|
cfg.NATS.ConsumerRules.DeliverPolicy = "last"
|
|
|
|
|
cfg.NATS.ConsumerRules.StartSequence = 0
|
|
|
|
|
cfg.NATS.ConsumerRules.StartTime = ""
|
|
|
|
|
case lower == "last_per_subject":
|
|
|
|
|
cfg.NATS.ConsumerRules.DeliverPolicy = "last_per_subject"
|
|
|
|
|
cfg.NATS.ConsumerRules.StartSequence = 0
|
|
|
|
|
cfg.NATS.ConsumerRules.StartTime = ""
|
|
|
|
|
case strings.HasPrefix(lower, "seq:"):
|
|
|
|
|
seqStr := strings.TrimPrefix(lower, "seq:")
|
|
|
|
|
if seq, err := strconv.ParseUint(seqStr, 10, 64); err == nil {
|
|
|
|
|
cfg.NATS.ConsumerRules.DeliverPolicy = "sequence"
|
|
|
|
|
cfg.NATS.ConsumerRules.StartSequence = seq
|
|
|
|
|
cfg.NATS.ConsumerRules.StartTime = ""
|
|
|
|
|
}
|
|
|
|
|
case strings.HasPrefix(lower, "time:"):
|
|
|
|
|
ts := strings.TrimSpace(value[5:])
|
|
|
|
|
if _, err := time.Parse(time.RFC3339, ts); err == nil {
|
|
|
|
|
cfg.NATS.ConsumerRules.DeliverPolicy = "time"
|
|
|
|
|
cfg.NATS.ConsumerRules.StartSequence = 0
|
|
|
|
|
cfg.NATS.ConsumerRules.StartTime = ts
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func initTelemetry(ctx context.Context, cfg *config.Config) (func(context.Context) error, error) {
|
|
|
|
|
if cfg == nil || !cfg.Telemetry.Enabled {
|
|
|
|
|
return func(context.Context) error { return nil }, nil
|
|
|
|
|
}
|
|
|
|
|
if cfg.Telemetry.Endpoint == "" {
|
|
|
|
|
return nil, fmt.Errorf("telemetry endpoint is required when telemetry.enabled=true")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
traceOpts := []otlptracehttp.Option{
|
|
|
|
|
otlptracehttp.WithEndpoint(cfg.Telemetry.Endpoint),
|
|
|
|
|
otlptracehttp.WithURLPath("/v1/traces"),
|
|
|
|
|
}
|
2025-11-18 11:47:35 +08:00
|
|
|
|
2025-11-15 09:01:24 +08:00
|
|
|
metricOpts := []otlpmetrichttp.Option{
|
|
|
|
|
otlpmetrichttp.WithEndpoint(cfg.Telemetry.Endpoint),
|
|
|
|
|
otlpmetrichttp.WithURLPath("/v1/metrics"),
|
|
|
|
|
}
|
2025-11-19 14:53:58 +08:00
|
|
|
|
|
|
|
|
if cfg.Telemetry.Insecure {
|
|
|
|
|
traceOpts = append(traceOpts, otlptracehttp.WithInsecure())
|
|
|
|
|
metricOpts = append(metricOpts, otlpmetrichttp.WithInsecure())
|
2025-11-15 09:01:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
traceExporter, err := otlptracehttp.New(ctx, traceOpts...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("init trace exporter: %w", err)
|
|
|
|
|
}
|
|
|
|
|
metricExporter, err := otlpmetrichttp.New(ctx, metricOpts...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("init metric exporter: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 11:47:35 +08:00
|
|
|
// Get environment for sampling configuration
|
2025-11-15 09:01:24 +08:00
|
|
|
env := os.Getenv("GO_ENV")
|
|
|
|
|
if env == "" {
|
|
|
|
|
env = "dev"
|
|
|
|
|
}
|
2025-11-18 11:47:35 +08:00
|
|
|
|
|
|
|
|
// Create comprehensive resource with service information
|
2025-11-15 09:01:24 +08:00
|
|
|
res, err := resource.New(ctx,
|
|
|
|
|
resource.WithFromEnv(),
|
|
|
|
|
resource.WithProcess(),
|
|
|
|
|
resource.WithOS(),
|
|
|
|
|
resource.WithHost(),
|
2025-11-18 11:47:35 +08:00
|
|
|
resource.WithContainer(),
|
2025-11-15 09:01:24 +08:00
|
|
|
resource.WithAttributes(
|
|
|
|
|
semconv.ServiceName("caatsm"),
|
2025-11-18 14:15:58 +08:00
|
|
|
semconv.ServiceVersion(buildinfo.Version),
|
2025-11-18 11:47:35 +08:00
|
|
|
semconv.ServiceNamespace("airport"),
|
|
|
|
|
attribute.String("service.component", "receiver"),
|
2025-11-15 09:01:24 +08:00
|
|
|
attribute.String("deployment.environment", env),
|
2025-11-18 11:47:35 +08:00
|
|
|
attribute.String("telemetry.endpoint", cfg.Telemetry.Endpoint),
|
|
|
|
|
attribute.Bool("telemetry.insecure", cfg.Telemetry.Insecure),
|
2025-11-15 09:01:24 +08:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("build telemetry resource: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 11:47:35 +08:00
|
|
|
// Configure sampling based on environment
|
|
|
|
|
sampler := getSamplerForEnvironment(env)
|
|
|
|
|
|
|
|
|
|
// Configure tracer provider with batching and sampling
|
2025-11-15 09:01:24 +08:00
|
|
|
tp := sdktrace.NewTracerProvider(
|
2025-11-18 11:47:35 +08:00
|
|
|
sdktrace.WithBatcher(traceExporter,
|
|
|
|
|
sdktrace.WithBatchTimeout(1*time.Second),
|
|
|
|
|
sdktrace.WithMaxExportBatchSize(512),
|
|
|
|
|
sdktrace.WithMaxQueueSize(2048),
|
|
|
|
|
),
|
2025-11-15 09:01:24 +08:00
|
|
|
sdktrace.WithResource(res),
|
2025-11-18 11:47:35 +08:00
|
|
|
sdktrace.WithSampler(sdktrace.ParentBased(sampler)),
|
2025-11-15 09:01:24 +08:00
|
|
|
)
|
2025-11-18 11:47:35 +08:00
|
|
|
|
|
|
|
|
// Configure meter provider with periodic reader
|
2025-11-15 09:01:24 +08:00
|
|
|
mp := sdkmetric.NewMeterProvider(
|
|
|
|
|
sdkmetric.WithResource(res),
|
2025-11-18 11:47:35 +08:00
|
|
|
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter,
|
|
|
|
|
sdkmetric.WithInterval(30*time.Second),
|
|
|
|
|
)),
|
2025-11-15 09:01:24 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
otel.SetTracerProvider(tp)
|
|
|
|
|
otel.SetMeterProvider(mp)
|
|
|
|
|
otel.SetTextMapPropagator(propagation.TraceContext{})
|
|
|
|
|
|
|
|
|
|
shutdown := func(ctx context.Context) error {
|
|
|
|
|
errs := []error{}
|
|
|
|
|
if err := mp.Shutdown(ctx); err != nil {
|
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
}
|
|
|
|
|
if err := tp.Shutdown(ctx); err != nil {
|
|
|
|
|
errs = append(errs, err)
|
|
|
|
|
}
|
|
|
|
|
return errors.Join(errs...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return shutdown, nil
|
|
|
|
|
}
|
2025-11-18 11:47:35 +08:00
|
|
|
|
|
|
|
|
// getSamplerForEnvironment returns appropriate sampling strategy for each environment
|
|
|
|
|
func getSamplerForEnvironment(env string) sdktrace.Sampler {
|
|
|
|
|
switch env {
|
|
|
|
|
case "prod", "production":
|
|
|
|
|
// 1% sampling in production to control costs and performance
|
|
|
|
|
return sdktrace.TraceIDRatioBased(0.01)
|
|
|
|
|
case "staging":
|
|
|
|
|
// 10% sampling in staging for better observability
|
|
|
|
|
return sdktrace.TraceIDRatioBased(0.1)
|
|
|
|
|
case "test", "testing":
|
|
|
|
|
// Always sample in testing for complete coverage
|
|
|
|
|
return sdktrace.AlwaysSample()
|
|
|
|
|
default:
|
|
|
|
|
// 100% sampling in development for debugging
|
|
|
|
|
return sdktrace.AlwaysSample()
|
|
|
|
|
}
|
|
|
|
|
}
|