Skip to content

Commit 2d48861

Browse files
committed
fix(query): Detect microsecond epoch timestamps correctly
Distinguish epoch units so microsecond trace timestamps no longer normalize near 1970 Add regression coverage for timestamp conversion and nested OpenTelemetry connection validation
1 parent bcb2670 commit 2d48861

3 files changed

Lines changed: 91 additions & 5 deletions

File tree

cmd/query/connections/service_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package connections
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/flanksource/commons-db/models"
@@ -67,6 +68,23 @@ func TestValidateOpenTelemetryRequiresNestedOpenSearchConnection(t *testing.T) {
6768
if err := validateNestedConnection(database, candidate); err == nil {
6869
t.Fatal("expected an HTTP URL ending in the unrelated connection name to fail")
6970
}
71+
// Namespaced, so it resolves to exactly one real record and reaches the
72+
// OpenSearch-only type check rather than failing earlier on self-reference
73+
// or a missing connection.
74+
relational := models.Connection{
75+
ID: uuid.New(), Name: "warehouse", Namespace: "team", Type: models.ConnectionTypePostgres,
76+
}
77+
if err := database.Create(&relational).Error; err != nil {
78+
t.Fatal(err)
79+
}
80+
candidate.Properties["connection"] = "connection://team/warehouse"
81+
err := validateNestedConnection(database, candidate)
82+
if err == nil {
83+
t.Fatal("expected a nested PostgreSQL connection to fail the OpenSearch-only check")
84+
}
85+
if !strings.Contains(err.Error(), models.ConnectionTypeOpenSearch) {
86+
t.Fatalf("expected the type mismatch to name the required type, got %v", err)
87+
}
7088
}
7189

7290
func TestApplyConnectionUpdatePreservesSecretWhenBlank(t *testing.T) {

query/providers/opentelemetry_parser.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,18 +201,33 @@ func traceDurationMillis(value any, field, format string) float64 {
201201
return number / 1_000_000
202202
}
203203

204+
// Epoch unit floors for normalizeTraceTimestamp. A timestamp in the supported
205+
// range (roughly 1973 onwards) exceeds the floor for its own unit and no other.
206+
const (
207+
epochMillisecondFloor = 10_000_000_000
208+
epochMicrosecondFloor = 10_000_000_000_000
209+
epochNanosecondFloor = 10_000_000_000_000_000
210+
)
211+
204212
func normalizeTraceTimestamp(raw string) string {
205213
if raw == "" {
206214
return ""
207215
}
208216
if number, err := strconv.ParseInt(raw, 10, 64); err == nil {
209-
if number > 10_000_000_000_000 {
217+
// Each epoch unit occupies a distinct magnitude band for any plausible
218+
// date, so the thresholds sit in the gaps between them. Microseconds
219+
// need their own band: without it a microsecond epoch (~1e15) is read
220+
// as nanoseconds and lands in the early 1970s.
221+
switch {
222+
case number > epochNanosecondFloor:
210223
return time.Unix(0, number).Format(time.RFC3339Nano)
211-
}
212-
if number > 10_000_000_000 {
224+
case number > epochMicrosecondFloor:
225+
return time.UnixMicro(number).Format(time.RFC3339Nano)
226+
case number > epochMillisecondFloor:
213227
return time.UnixMilli(number).Format(time.RFC3339Nano)
228+
default:
229+
return time.Unix(number, 0).Format(time.RFC3339Nano)
214230
}
215-
return time.Unix(number, 0).Format(time.RFC3339Nano)
216231
}
217232
if timestamp, err := time.Parse(time.RFC3339Nano, raw); err == nil {
218233
return timestamp.Format(time.RFC3339Nano)

query/providers/opentelemetry_parser_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package providers
22

3-
import "testing"
3+
import (
4+
"strconv"
5+
"testing"
6+
"time"
7+
)
48

59
func TestTraceDurationMillisUsesFieldUnits(t *testing.T) {
610
tests := []struct {
@@ -22,3 +26,52 @@ func TestTraceDurationMillisUsesFieldUnits(t *testing.T) {
2226
})
2327
}
2428
}
29+
30+
// Every epoch unit must round-trip to the same instant. Microseconds are the
31+
// regression: without their own magnitude band they were read as nanoseconds
32+
// and normalized to 1970.
33+
func TestNormalizeTraceTimestampDetectsEpochUnit(t *testing.T) {
34+
instant := time.Date(2026, time.July, 27, 8, 42, 43, 0, time.UTC)
35+
want := instant.Format(time.RFC3339Nano)
36+
37+
tests := []struct {
38+
name string
39+
raw int64
40+
}{
41+
{name: "seconds", raw: instant.Unix()},
42+
{name: "milliseconds", raw: instant.UnixMilli()},
43+
{name: "microseconds", raw: instant.UnixMicro()},
44+
{name: "nanoseconds", raw: instant.UnixNano()},
45+
}
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
got := normalizeTraceTimestamp(strconv.FormatInt(tt.raw, 10))
49+
parsed, err := time.Parse(time.RFC3339Nano, got)
50+
if err != nil {
51+
t.Fatalf("normalizeTraceTimestamp(%d) = %q, not a timestamp: %v", tt.raw, got, err)
52+
}
53+
if !parsed.UTC().Equal(instant) {
54+
t.Fatalf("normalizeTraceTimestamp(%d) = %q, want %q", tt.raw, parsed.UTC().Format(time.RFC3339Nano), want)
55+
}
56+
})
57+
}
58+
}
59+
60+
func TestNormalizeTraceTimestampPassesThroughNonEpochValues(t *testing.T) {
61+
tests := []struct {
62+
name string
63+
raw string
64+
want string
65+
}{
66+
{name: "empty", raw: "", want: ""},
67+
{name: "rfc3339", raw: "2026-07-27T08:42:43Z", want: "2026-07-27T08:42:43Z"},
68+
{name: "unparseable", raw: "not-a-timestamp", want: "not-a-timestamp"},
69+
}
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
if got := normalizeTraceTimestamp(tt.raw); got != tt.want {
73+
t.Fatalf("normalizeTraceTimestamp(%q) = %q, want %q", tt.raw, got, tt.want)
74+
}
75+
})
76+
}
77+
}

0 commit comments

Comments
 (0)