-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtimestamp.go
More file actions
213 lines (179 loc) · 5.69 KB
/
Copy pathtimestamp.go
File metadata and controls
213 lines (179 loc) · 5.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package timestamp
import (
"encoding/binary"
"fmt"
"io"
"log/slog"
"net"
"os"
"sync"
"time"
)
// TimeServer environment variable for NTP server.
const TimeServer = "FGRZL_TIME_SERVER"
var (
globalClock *clock
once sync.Once
initErr error
logger *slog.Logger
)
// clock holds the start time for the monotonic clock.
// All fields are immutable after initialization for thread safety.
type clock struct {
startTime int64 // Unix timestamp in milliseconds
start time.Time // Monotonic reference point
mu sync.RWMutex // Protects against potential races during reads
}
func init() {
// Initialize with a default logger that can be overridden
logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelWarn, // Only log warnings and errors by default
})).With("component", "timestamp")
}
// SetLogger allows users to control logging behavior.
// Pass nil to disable logging entirely.
func SetLogger(l *slog.Logger) {
if l == nil {
// Create a logger that discards all output
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
return
}
logger = l.With("component", "timestamp")
}
// DisableLogging is a convenience function to disable all logging output.
func DisableLogging() {
SetLogger(nil)
}
// Initialize the global clock once during application startup.
func init() {
once.Do(func() {
defer func() {
if r := recover(); r != nil {
// If initialization panics, fall back to system time
logger.Error("initialization panic, falling back to system time",
"error", r)
globalClock = &clock{
startTime: time.Now().UnixMilli(),
start: time.Now(),
}
initErr = fmt.Errorf("initialization panic: %v", r)
}
}()
t, err := getCurrentTime()
if err != nil {
initErr = err
// Even on error, we still have a valid fallback time
}
globalClock = &clock{
startTime: t.UnixMilli(),
start: t,
}
})
}
// GetTimestamp returns a timestamp using monotonic elapsed time.
// This function is thread-safe and guaranteed to return monotonically increasing values.
func GetTimestamp() int64 {
if globalClock == nil {
// Fallback if initialization somehow failed completely
return time.Now().UnixMilli()
}
globalClock.mu.RLock()
defer globalClock.mu.RUnlock()
elapsed := time.Since(globalClock.start)
return globalClock.startTime + elapsed.Milliseconds()
}
// GetInitializationError returns any error that occurred during initialization.
// Returns nil if initialization was successful.
func GetInitializationError() error {
return initErr
}
// GetTimeServer fetches the configured NTP server from the environment.
func GetTimeServer() string {
return os.Getenv(TimeServer)
}
// Default list of NTP servers.
var ntpServers = []string{
"time.google.com:123",
"time.aws.com:123",
"time.cloudflare.com:123",
"time.windows.com:123",
}
// getCurrentTime attempts to fetch time from NTP or falls back to system time.
// Returns the time and any error encountered (for logging purposes).
func getCurrentTime() (time.Time, error) {
server := GetTimeServer()
if server == "system" {
return time.Now(), nil
}
if server == "default" || server == "" {
var lastErr error
for _, s := range ntpServers {
t, err := ntpTime(s)
if err == nil {
return t, nil
}
logger.Warn("NTP server failed",
"server", s,
"error", err)
lastErr = err
}
logger.Warn("All NTP servers failed, falling back to system time",
"last_error", lastErr)
return time.Now(), fmt.Errorf("all NTP servers failed, last error: %w", lastErr)
}
ntpTime, err := ntpTime(server)
if err != nil {
logger.Warn("Failed to fetch time from NTP server, falling back to system time",
"server", server,
"error", err)
return time.Now(), fmt.Errorf("NTP server %s failed: %w", server, err)
}
return ntpTime, nil
}
// ntpTime fetches the current time from the specified NTP server.
// Includes proper timeout handling and validates NTP response.
func ntpTime(server string) (time.Time, error) {
addr, err := net.ResolveUDPAddr("udp", server)
if err != nil {
return time.Time{}, fmt.Errorf("failed to resolve address: %w", err)
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return time.Time{}, fmt.Errorf("failed to connect: %w", err)
}
defer func() { _ = conn.Close() }()
// Set a reasonable timeout for the NTP request
deadline := time.Now().Add(5 * time.Second)
if err := conn.SetDeadline(deadline); err != nil {
logger.Warn("failed to set deadline", "error", err)
}
req := make([]byte, 48)
req[0] = 0x1B // NTP version 3, client mode
if _, err = conn.Write(req); err != nil {
return time.Time{}, fmt.Errorf("failed to send request: %w", err)
}
resp := make([]byte, 48)
if _, err = conn.Read(resp); err != nil {
return time.Time{}, fmt.Errorf("failed to read response: %w", err)
}
// Validate NTP response
if len(resp) < 48 {
return time.Time{}, fmt.Errorf("invalid NTP response length: %d", len(resp))
}
// Extract transmit timestamp (bytes 40-47)
seconds := binary.BigEndian.Uint32(resp[40:44])
fraction := binary.BigEndian.Uint32(resp[44:48])
// Validate that we got a reasonable response
if seconds == 0 {
return time.Time{}, fmt.Errorf("invalid NTP response: zero timestamp")
}
ntpSeconds := float64(seconds) + float64(fraction)/0x100000000
unixSeconds := ntpSeconds - 2208988800 // NTP epoch offset (1900-01-01 to 1970-01-01)
ntpTime := time.Unix(int64(unixSeconds), 0)
// Sanity check: ensure the time is reasonable (not too far in past/future)
now := time.Now()
if ntpTime.Before(now.Add(-24*time.Hour)) || ntpTime.After(now.Add(24*time.Hour)) {
return time.Time{}, fmt.Errorf("NTP time %v is too far from system time %v", ntpTime, now)
}
return ntpTime, nil
}