mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-07 07:00:38 +01:00
Compare commits
4 Commits
feat/dashb
...
issue_5123
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
599ae273c9 | ||
|
|
36edbeca38 | ||
|
|
0436655a43 | ||
|
|
301d496092 |
@@ -178,6 +178,8 @@ func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
queryRangeRequest.Normalize()
|
||||
|
||||
// Validate the query request
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
|
||||
@@ -91,11 +91,10 @@ func New(
|
||||
|
||||
func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error) {
|
||||
|
||||
// Coerce the window to epoch milliseconds up front so every downstream
|
||||
// consumer (TimeRange, narrowWindowByTraceID, step interval, etc.) can
|
||||
// safely assume ms regardless of the resolution the caller sent.
|
||||
req.Start = querybuilder.ToMilliSecs(req.Start)
|
||||
req.End = querybuilder.ToMilliSecs(req.End)
|
||||
// Normalize Start/End to ms. UnmarshalJSON covers HTTP requests; callers
|
||||
// that build the request programmatically skip it, so this is the catch-all
|
||||
// (idempotent for the already-normalized path).
|
||||
req.Normalize()
|
||||
|
||||
event := &qbtypes.QBEvent{
|
||||
Version: "v5",
|
||||
@@ -456,10 +455,9 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
|
||||
func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream) {
|
||||
|
||||
// Coerce the window to epoch milliseconds up front (End may be 0 for the
|
||||
// open-ended stream, which ToMilliSecs leaves untouched).
|
||||
req.Start = querybuilder.ToMilliSecs(req.Start)
|
||||
req.End = querybuilder.ToMilliSecs(req.End)
|
||||
// Catch-all normalization for programmatic callers (see QueryRange). End is
|
||||
// 0 here for the open-ended stream, which Normalize leaves untouched.
|
||||
req.Normalize()
|
||||
|
||||
event := &qbtypes.QBEvent{
|
||||
Version: "v5",
|
||||
|
||||
@@ -33,28 +33,6 @@ func ToNanoSecs(epoch uint64) uint64 {
|
||||
return temp * uint64(math.Pow(10, float64(19-count)))
|
||||
}
|
||||
|
||||
// ToMilliSecs takes an epoch whose resolution is inferred from its magnitude
|
||||
// (s/ms/µs/ns) and returns it in milliseconds. A millisecond epoch for the
|
||||
// current era has 13 digits (e.g. ~1.7e12 in 2026), so the value is scaled so
|
||||
// its digit-width matches: smaller values (seconds) are scaled up, larger ones
|
||||
// (micro/nanoseconds) are scaled down. Zero is returned unchanged.
|
||||
func ToMilliSecs(epoch uint64) uint64 {
|
||||
if epoch == 0 {
|
||||
return 0
|
||||
}
|
||||
temp := epoch
|
||||
count := 0
|
||||
for epoch != 0 {
|
||||
epoch /= 10
|
||||
count++
|
||||
}
|
||||
const msDigits = 13
|
||||
if count < msDigits {
|
||||
return temp * uint64(math.Pow(10, float64(msDigits-count)))
|
||||
}
|
||||
return temp / uint64(math.Pow(10, float64(count-msDigits)))
|
||||
}
|
||||
|
||||
// TODO(srikanthccv): should these be rounded to nearest multiple of 60 instead of 5 if step > 60?
|
||||
// That would make graph look nice but "nice" but should be less important than the usefulness.
|
||||
func RecommendedStepInterval(start, end uint64) uint64 {
|
||||
|
||||
@@ -60,51 +60,3 @@ func TestToNanoSecs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToMilliSecs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
epoch uint64
|
||||
expected uint64
|
||||
}{
|
||||
{
|
||||
name: "10-digit Unix timestamp (seconds) - 2023-01-01 00:00:00 UTC",
|
||||
epoch: 1672531200, // seconds
|
||||
expected: 1672531200000, // * 10^3
|
||||
},
|
||||
{
|
||||
name: "13-digit Unix timestamp (milliseconds) - already ms",
|
||||
epoch: 1672531200000,
|
||||
expected: 1672531200000, // unchanged
|
||||
},
|
||||
{
|
||||
name: "16-digit Unix timestamp (microseconds)",
|
||||
epoch: 1672531200000000, // microseconds
|
||||
expected: 1672531200000, // / 10^3
|
||||
},
|
||||
{
|
||||
name: "19-digit Unix timestamp (nanoseconds)",
|
||||
epoch: 1672531200000000000, // nanoseconds
|
||||
expected: 1672531200000, // / 10^6
|
||||
},
|
||||
{
|
||||
name: "Unix epoch start - zero is unchanged",
|
||||
epoch: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "Recent timestamp in seconds - 2024-05-25 12:00:00 UTC",
|
||||
epoch: 1716638400,
|
||||
expected: 1716638400000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ToMilliSecs(tt.epoch)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ToMilliSecs(%d) = %d, want %d", tt.epoch, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,7 +614,14 @@ func (r *QueryRangeRequest) SkipFillGaps(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshaling to disallow unknown fields.
|
||||
// Normalize coerces Start and End to epoch milliseconds, inferring the source
|
||||
// resolution (s/ms/µs/ns) from each value's magnitude. Lets downstream consumers
|
||||
// assume ms regardless of what the caller sent. This is a pure transformation;
|
||||
// range validation of the resulting values lives in Validate.
|
||||
func (r *QueryRangeRequest) Normalize() {
|
||||
r.Start, r.End = toMilliSecs(r.Start), toMilliSecs(r.End)
|
||||
}
|
||||
|
||||
func (r *QueryRangeRequest) UnmarshalJSON(data []byte) error {
|
||||
// Define a type alias to avoid infinite recursion
|
||||
type Alias QueryRangeRequest
|
||||
@@ -655,6 +662,10 @@ func (r *QueryRangeRequest) UnmarshalJSON(data []byte) error {
|
||||
// Copy the decoded values back to the original struct
|
||||
*r = QueryRangeRequest(temp)
|
||||
|
||||
// Coerce Start/End to ms at decode time for HTTP requests. Range validation
|
||||
// of the coerced values happens later in Validate.
|
||||
r.Normalize()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -708,3 +719,19 @@ func (r *QueryRangeRequest) GetQueriesSupportingZeroDefault() map[string]bool {
|
||||
|
||||
return canDefaultZero
|
||||
}
|
||||
|
||||
// toMilliSecs scales an epoch to milliseconds based on its magnitude: seconds are
|
||||
// scaled up, micro/nanoseconds down, milliseconds left as-is. Zero is returned
|
||||
// unchanged.
|
||||
func toMilliSecs(epoch uint64) uint64 {
|
||||
switch {
|
||||
case epoch < 1e12: // seconds
|
||||
return epoch * 1_000
|
||||
case epoch < 1e15: // milliseconds
|
||||
return epoch
|
||||
case epoch < 1e18: // microseconds
|
||||
return epoch / 1_000
|
||||
default: // nanoseconds
|
||||
return epoch / 1_000_000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1903,3 +1903,114 @@ func TestQueryRangeRequest_StepIntervalForQuery(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRangeRequest_Normalize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
start uint64
|
||||
end uint64
|
||||
wantStart uint64
|
||||
wantEnd uint64
|
||||
}{
|
||||
{
|
||||
name: "seconds are scaled up to ms",
|
||||
start: 1672531200, // 2023-01-01 in seconds
|
||||
end: 1716638400, // 2024-05-25 in seconds
|
||||
wantStart: 1672531200000, // * 10^3
|
||||
wantEnd: 1716638400000,
|
||||
},
|
||||
{
|
||||
name: "milliseconds pass through unchanged",
|
||||
start: 1672531200000,
|
||||
end: 1716638400000,
|
||||
wantStart: 1672531200000,
|
||||
wantEnd: 1716638400000,
|
||||
},
|
||||
{
|
||||
name: "microseconds are scaled down to ms",
|
||||
start: 1672531200000000, // µs
|
||||
end: 1716638400000000,
|
||||
wantStart: 1672531200000, // / 10^3
|
||||
wantEnd: 1716638400000,
|
||||
},
|
||||
{
|
||||
name: "nanoseconds are scaled down to ms",
|
||||
start: 1672531200000000000, // ns
|
||||
end: 1716638400000000000,
|
||||
wantStart: 1672531200000, // / 10^6
|
||||
wantEnd: 1716638400000,
|
||||
},
|
||||
{
|
||||
name: "zero end (open-ended stream) is left untouched",
|
||||
start: 1672531200000,
|
||||
end: 0,
|
||||
wantStart: 1672531200000,
|
||||
wantEnd: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &QueryRangeRequest{Start: tt.start, End: tt.end}
|
||||
r.Normalize()
|
||||
assert.Equal(t, tt.wantStart, r.Start)
|
||||
assert.Equal(t, tt.wantEnd, r.End)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRangeRequest_ValidateTimeRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
start uint64
|
||||
end uint64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "in-range ms timestamps pass",
|
||||
start: 1672531200000, // 2023-01-01
|
||||
end: 1716638400000, // 2024-05-25
|
||||
},
|
||||
{
|
||||
name: "start below the 1990 bound is rejected",
|
||||
start: 1000, // ~1970
|
||||
end: 1716638400000,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "end beyond the 2100 bound is rejected",
|
||||
start: 1672531200000,
|
||||
end: 5_000_000_000_000, // ~year 2128 in ms
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &QueryRangeRequest{
|
||||
Start: tt.start,
|
||||
End: tt.end,
|
||||
RequestType: RequestTypeTimeSeries,
|
||||
CompositeQuery: CompositeQuery{
|
||||
Queries: []QueryEnvelope{
|
||||
{
|
||||
Type: QueryTypeBuilder,
|
||||
Spec: QueryBuilderQuery[LogAggregation]{
|
||||
Name: "A",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Aggregations: []LogAggregation{{Expression: "count()"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := r.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "outside the supported range")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
// minEpochMs and maxEpochMs bound a plausible ms timestamp to
|
||||
// 1990-01-01 .. 2100-01-01, used to reject malformed Start/End values.
|
||||
minEpochMs uint64 = 631_152_000_000
|
||||
maxEpochMs uint64 = 4_102_444_800_000
|
||||
)
|
||||
|
||||
// getQueryIdentifier returns a friendly identifier for a query based on its type and name/content.
|
||||
func getQueryIdentifier(envelope QueryEnvelope, index int) string {
|
||||
name := envelope.GetQueryName()
|
||||
@@ -527,7 +534,24 @@ func (q *QueryBuilderQuery[T]) validateOrderByForAggregation() error {
|
||||
|
||||
// Validate validates the entire query range request.
|
||||
func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
|
||||
// Validate time range
|
||||
// Validate time range. Start/End are assumed to be normalized to ms (see
|
||||
// QueryRangeRequest.Normalize); reject non-zero values outside the plausible
|
||||
// 1990-2100 range as malformed.
|
||||
if r.Start != 0 && (r.Start < minEpochMs || r.Start > maxEpochMs) {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"start timestamp %d is outside the supported range (1990-2100)",
|
||||
r.Start,
|
||||
)
|
||||
}
|
||||
if r.End != 0 && (r.End < minEpochMs || r.End > maxEpochMs) {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"end timestamp %d is outside the supported range (1990-2100)",
|
||||
r.End,
|
||||
)
|
||||
}
|
||||
|
||||
if r.RequestType != RequestTypeRawStream && r.Start >= r.End {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
|
||||
Reference in New Issue
Block a user