mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 23:47:12 +00:00
* feat: refactor tracefunnel to support dynamic multi-step funnels Replace hardcoded 2-step and 3-step funnel functions with dynamic implementations that support unlimited steps. Add comprehensive tests for multi-step funnel functionality while maintaining backward compatibility. Key changes: - Add dynamic query builders for n-step funnels - Update all query functions to use new builders - Remove old hardcoded functions - Add tests for 1-6 step funnels - Maintain temporal ordering logic 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add duration calculation for latency_pointer='end' in funnel qu… (#8632) * feat: add duration calculation for latency_pointer='end' in funnel queries - Updated BuildFunnelOverviewQuery and BuildFunnelStepOverviewQuery to calculate end time when latency_pointer is 'end' - Modified BuildFunnelTopSlowTracesQuery and BuildFunnelTopSlowErrorTracesQuery to support latency pointer parameters - Added comprehensive tests for latency pointer functionality in clickhouse_queries_latency_test.go - When latency_pointer is 'end', the query now adds span duration to timestamp for accurate latency calculations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * do matching after lowercase conversion Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: Ankit Nayan <ankitnayan@Ankits-MacBook-Pro.local> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix: apply remaining changes from PR #8615 for ClickHouse 25.5 compatibility - Updated BuildTracesFilter to BuildTracesFilterQuery with false parameter in query.go - Updated test files to expect resource_string_service$$name instead of serviceName - Fixed function reference in query_test.go These changes complete the ClickHouse 25.5 compatibility updates while maintaining the dynamic multi-step funnel functionality. * fix: replace durationNano with duration_nano for ClickHouse compatibility - Updated all SQL queries in clickhouse_queries.go to use duration_nano column name - Updated test expectations in clickhouse_queries_latency_test.go - Ensures consistency with ClickHouse snake_case column naming convention * refactor: code formatting and add TODO comment - Remove trailing whitespace in query.go - Add TODO comment for GetErroredTraces function regarding product improvement - Add newline at end of file for proper formatting --------- Co-authored-by: Ankit Nayan <ankitnayan@Ankits-MacBook-Pro.local> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
329 lines
8.4 KiB
Go
329 lines
8.4 KiB
Go
package tracefunnel
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tracev4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
|
|
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
|
"github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
|
)
|
|
|
|
// sanitizeClause adds AND prefix to non-empty clauses if not already present
|
|
func sanitizeClause(clause string) string {
|
|
if clause == "" {
|
|
return ""
|
|
}
|
|
// Check if clause already starts with AND
|
|
if strings.HasPrefix(strings.TrimSpace(clause), "AND") {
|
|
return clause
|
|
}
|
|
return "AND " + clause
|
|
}
|
|
|
|
func ValidateTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
|
funnelSteps := funnel.Steps
|
|
|
|
// Build step data for the dynamic query builder
|
|
steps := make([]struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
Clause string
|
|
}, len(funnelSteps))
|
|
|
|
for i, step := range funnelSteps {
|
|
// Build filter clause
|
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
steps[i] = struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
Clause string
|
|
}{
|
|
ServiceName: step.ServiceName,
|
|
SpanName: step.SpanName,
|
|
ContainsError: 0,
|
|
Clause: sanitizeClause(clause),
|
|
}
|
|
|
|
if step.HasErrors {
|
|
steps[i].ContainsError = 1
|
|
}
|
|
}
|
|
|
|
query := BuildFunnelValidationQuery(steps, timeRange.StartTime, timeRange.EndTime)
|
|
|
|
return &v3.ClickHouseQuery{
|
|
Query: query,
|
|
}, nil
|
|
}
|
|
|
|
func GetFunnelAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
|
funnelSteps := funnel.Steps
|
|
|
|
// Build step data for the dynamic query builder
|
|
steps := make([]struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
LatencyPointer string
|
|
Clause string
|
|
}, len(funnelSteps))
|
|
|
|
for i, step := range funnelSteps {
|
|
// Build filter clause
|
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
latencyPointer := step.LatencyPointer
|
|
if latencyPointer == "" {
|
|
latencyPointer = "start"
|
|
}
|
|
|
|
steps[i] = struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
LatencyPointer string
|
|
Clause string
|
|
}{
|
|
ServiceName: step.ServiceName,
|
|
SpanName: step.SpanName,
|
|
ContainsError: 0,
|
|
LatencyPointer: latencyPointer,
|
|
Clause: sanitizeClause(clause),
|
|
}
|
|
|
|
if step.HasErrors {
|
|
steps[i].ContainsError = 1
|
|
}
|
|
}
|
|
|
|
query := BuildFunnelOverviewQuery(steps, timeRange.StartTime, timeRange.EndTime)
|
|
|
|
return &v3.ClickHouseQuery{Query: query}, nil
|
|
}
|
|
|
|
func GetFunnelStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
|
if stepStart == stepEnd {
|
|
return nil, fmt.Errorf("step start and end cannot be the same for /step/overview")
|
|
}
|
|
|
|
funnelSteps := funnel.Steps
|
|
|
|
// Build step data for the dynamic query builder
|
|
steps := make([]struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
LatencyPointer string
|
|
LatencyType string
|
|
Clause string
|
|
}, len(funnelSteps))
|
|
|
|
for i, step := range funnelSteps {
|
|
// Build filter clause
|
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
latencyPointer := step.LatencyPointer
|
|
if latencyPointer == "" {
|
|
latencyPointer = "start"
|
|
}
|
|
|
|
steps[i] = struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
LatencyPointer string
|
|
LatencyType string
|
|
Clause string
|
|
}{
|
|
ServiceName: step.ServiceName,
|
|
SpanName: step.SpanName,
|
|
ContainsError: 0,
|
|
LatencyPointer: latencyPointer,
|
|
LatencyType: step.LatencyType,
|
|
Clause: sanitizeClause(clause),
|
|
}
|
|
|
|
if step.HasErrors {
|
|
steps[i].ContainsError = 1
|
|
}
|
|
}
|
|
|
|
query := BuildFunnelStepOverviewQuery(steps, timeRange.StartTime, timeRange.EndTime, stepStart, stepEnd)
|
|
|
|
return &v3.ClickHouseQuery{Query: query}, nil
|
|
}
|
|
|
|
func GetStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
|
funnelSteps := funnel.Steps
|
|
|
|
// Build step data for the dynamic query builder
|
|
steps := make([]struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
Clause string
|
|
}, len(funnelSteps))
|
|
|
|
for i, step := range funnelSteps {
|
|
// Build filter clause
|
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
steps[i] = struct {
|
|
ServiceName string
|
|
SpanName string
|
|
ContainsError int
|
|
Clause string
|
|
}{
|
|
ServiceName: step.ServiceName,
|
|
SpanName: step.SpanName,
|
|
ContainsError: 0,
|
|
Clause: sanitizeClause(clause),
|
|
}
|
|
|
|
if step.HasErrors {
|
|
steps[i].ContainsError = 1
|
|
}
|
|
}
|
|
|
|
query := BuildFunnelCountQuery(steps, timeRange.StartTime, timeRange.EndTime)
|
|
|
|
return &v3.ClickHouseQuery{
|
|
Query: query,
|
|
}, nil
|
|
}
|
|
|
|
func GetSlowestTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
|
funnelSteps := funnel.Steps
|
|
containsErrorT1 := 0
|
|
containsErrorT2 := 0
|
|
stepStartOrder := 0
|
|
stepEndOrder := 1
|
|
|
|
if stepStart != stepEnd {
|
|
stepStartOrder = int(stepStart) - 1
|
|
stepEndOrder = int(stepEnd) - 1
|
|
if funnelSteps[stepStartOrder].HasErrors {
|
|
containsErrorT1 = 1
|
|
}
|
|
if funnelSteps[stepEndOrder].HasErrors {
|
|
containsErrorT2 = 1
|
|
}
|
|
}
|
|
|
|
// Build filter clauses for the steps
|
|
clauseStep1, err := tracev4.BuildTracesFilterQuery(funnelSteps[stepStartOrder].Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[stepEndOrder].Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sanitize clauses
|
|
clauseStep1 = sanitizeClause(clauseStep1)
|
|
clauseStep2 = sanitizeClause(clauseStep2)
|
|
|
|
// Get latency pointers with defaults
|
|
latencyPointerT1 := funnelSteps[stepStartOrder].LatencyPointer
|
|
if latencyPointerT1 == "" {
|
|
latencyPointerT1 = "start"
|
|
}
|
|
latencyPointerT2 := funnelSteps[stepEndOrder].LatencyPointer
|
|
if latencyPointerT2 == "" {
|
|
latencyPointerT2 = "start"
|
|
}
|
|
|
|
query := BuildFunnelTopSlowTracesQuery(
|
|
containsErrorT1,
|
|
containsErrorT2,
|
|
timeRange.StartTime,
|
|
timeRange.EndTime,
|
|
funnelSteps[stepStartOrder].ServiceName,
|
|
funnelSteps[stepStartOrder].SpanName,
|
|
funnelSteps[stepEndOrder].ServiceName,
|
|
funnelSteps[stepEndOrder].SpanName,
|
|
clauseStep1,
|
|
clauseStep2,
|
|
latencyPointerT1,
|
|
latencyPointerT2,
|
|
)
|
|
return &v3.ClickHouseQuery{Query: query}, nil
|
|
}
|
|
|
|
// TODO: Showing traces with error which are slow makes little sense as a product. We should show the error spans directly in the funnel chart. Rather showing traces which has drop between steps will be more relevant
|
|
func GetErroredTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
|
funnelSteps := funnel.Steps
|
|
containsErrorT1 := 0
|
|
containsErrorT2 := 0
|
|
stepStartOrder := 0
|
|
stepEndOrder := 1
|
|
|
|
if stepStart != stepEnd {
|
|
stepStartOrder = int(stepStart) - 1
|
|
stepEndOrder = int(stepEnd) - 1
|
|
if funnelSteps[stepStartOrder].HasErrors {
|
|
containsErrorT1 = 1
|
|
}
|
|
if funnelSteps[stepEndOrder].HasErrors {
|
|
containsErrorT2 = 1
|
|
}
|
|
}
|
|
|
|
// Build filter clauses for the steps
|
|
clauseStep1, err := tracev4.BuildTracesFilterQuery(funnelSteps[stepStartOrder].Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[stepEndOrder].Filters, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sanitize clauses
|
|
clauseStep1 = sanitizeClause(clauseStep1)
|
|
clauseStep2 = sanitizeClause(clauseStep2)
|
|
|
|
// Get latency pointers with defaults
|
|
latencyPointerT1 := funnelSteps[stepStartOrder].LatencyPointer
|
|
if latencyPointerT1 == "" {
|
|
latencyPointerT1 = "start"
|
|
}
|
|
latencyPointerT2 := funnelSteps[stepEndOrder].LatencyPointer
|
|
if latencyPointerT2 == "" {
|
|
latencyPointerT2 = "start"
|
|
}
|
|
|
|
query := BuildFunnelTopSlowErrorTracesQuery(
|
|
containsErrorT1,
|
|
containsErrorT2,
|
|
timeRange.StartTime,
|
|
timeRange.EndTime,
|
|
funnelSteps[stepStartOrder].ServiceName,
|
|
funnelSteps[stepStartOrder].SpanName,
|
|
funnelSteps[stepEndOrder].ServiceName,
|
|
funnelSteps[stepEndOrder].SpanName,
|
|
clauseStep1,
|
|
clauseStep2,
|
|
latencyPointerT1,
|
|
latencyPointerT2,
|
|
)
|
|
return &v3.ClickHouseQuery{Query: query}, nil
|
|
}
|