github.com/grafana/pyroscope@v1.18.0/pkg/model/time_series_builder_test.go (about)

     1  package model
     2  
     3  import (
     4  	"sort"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    11  	schemav1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1"
    12  )
    13  
    14  func TestTimeSeriesBuilder_NoExemplarsForEmptyProfileID(t *testing.T) {
    15  	builder := NewTimeSeriesBuilder()
    16  	labels := Labels{
    17  		{Name: "service_name", Value: "api"},
    18  		{Name: "env", Value: "prod"},
    19  	}
    20  
    21  	builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "")
    22  	builder.Add(1, labels, 1000, 200.0, schemav1.Annotations{}, "")
    23  
    24  	series := builder.BuildWithExemplars()
    25  	require.Len(t, series, 1)
    26  	require.Len(t, series[0].Points, 2)
    27  
    28  	for _, point := range series[0].Points {
    29  		assert.Empty(t, point.Exemplars, "Empty profileID should not create exemplars")
    30  	}
    31  }
    32  
    33  func TestTimeSeriesBuilder_Build_NoExemplars(t *testing.T) {
    34  	builder := NewTimeSeriesBuilder()
    35  	labels := Labels{
    36  		{Name: "service_name", Value: "api"},
    37  	}
    38  
    39  	builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-1")
    40  
    41  	series := builder.Build()
    42  	require.Len(t, series, 1)
    43  	require.Len(t, series[0].Points, 1)
    44  	assert.Empty(t, series[0].Points[0].Exemplars, "Build() should not attach exemplars")
    45  }
    46  
    47  func TestTimeSeriesBuilder_BuildWithExemplars_AttachesExemplars(t *testing.T) {
    48  	builder := NewTimeSeriesBuilder()
    49  	labels := Labels{
    50  		{Name: "service_name", Value: "api"},
    51  		{Name: "pod", Value: "pod-123"},
    52  	}
    53  
    54  	builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-1")
    55  
    56  	series := builder.BuildWithExemplars()
    57  	require.Len(t, series, 1)
    58  	require.Len(t, series[0].Points, 1)
    59  	require.Len(t, series[0].Points[0].Exemplars, 1)
    60  
    61  	exemplar := series[0].Points[0].Exemplars[0]
    62  	assert.Equal(t, "profile-1", exemplar.ProfileId)
    63  	assert.Equal(t, uint64(100), exemplar.Value)
    64  	assert.Equal(t, int64(1000), exemplar.Timestamp)
    65  
    66  	assert.Len(t, exemplar.Labels, 2)
    67  	assert.Equal(t, "api", findLabelValue(exemplar.Labels, "service_name"))
    68  	assert.Equal(t, "pod-123", findLabelValue(exemplar.Labels, "pod"))
    69  }
    70  
    71  func TestTimeSeriesBuilder_MultipleExemplarsAtSameTimestamp(t *testing.T) {
    72  	builder := NewTimeSeriesBuilder()
    73  	labels := Labels{
    74  		{Name: "service_name", Value: "api"},
    75  	}
    76  
    77  	builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-1")
    78  	builder.Add(1, labels, 1000, 200.0, schemav1.Annotations{}, "profile-2")
    79  	builder.Add(1, labels, 1000, 300.0, schemav1.Annotations{}, "profile-3")
    80  
    81  	series := builder.BuildWithExemplars()
    82  	require.Len(t, series, 1)
    83  	require.Len(t, series[0].Points, 3)
    84  
    85  	// All 3 points at timestamp 1000 should have all 3 exemplars
    86  	for _, point := range series[0].Points {
    87  		require.Len(t, point.Exemplars, 3)
    88  		profileIDs := make(map[string]bool)
    89  		for _, ex := range point.Exemplars {
    90  			profileIDs[ex.ProfileId] = true
    91  		}
    92  		assert.True(t, profileIDs["profile-1"])
    93  		assert.True(t, profileIDs["profile-2"])
    94  		assert.True(t, profileIDs["profile-3"])
    95  	}
    96  }
    97  
    98  func TestTimeSeriesBuilder_GroupBy(t *testing.T) {
    99  	builder := NewTimeSeriesBuilder("service_name")
   100  	labels1 := Labels{
   101  		{Name: "service_name", Value: "api"},
   102  		{Name: "pod", Value: "pod-1"},
   103  	}
   104  	labels2 := Labels{
   105  		{Name: "service_name", Value: "api"},
   106  		{Name: "pod", Value: "pod-2"},
   107  	}
   108  
   109  	builder.Add(1, labels1, 1000, 100.0, schemav1.Annotations{}, "profile-1")
   110  	builder.Add(2, labels2, 1000, 200.0, schemav1.Annotations{}, "profile-2")
   111  
   112  	series := builder.BuildWithExemplars()
   113  
   114  	// Should be grouped into 1 series by service_name
   115  	require.Len(t, series, 1)
   116  	assert.Len(t, series[0].Labels, 1)
   117  	assert.Equal(t, "service_name", series[0].Labels[0].Name)
   118  	assert.Equal(t, "api", series[0].Labels[0].Value)
   119  
   120  	require.Len(t, series[0].Points, 2)
   121  
   122  	// Both exemplars should be at timestamp 1000, grouped together
   123  	point := series[0].Points[0]
   124  	require.Len(t, point.Exemplars, 2)
   125  
   126  	// Exemplars should have only non-grouped labels (pod), not service_name
   127  	for _, ex := range point.Exemplars {
   128  		assert.Len(t, ex.Labels, 1)
   129  		assert.NotEmpty(t, findLabelValue(ex.Labels, "pod"))
   130  		assert.Empty(t, findLabelValue(ex.Labels, "service_name"))
   131  	}
   132  }
   133  
   134  func TestTimeSeriesBuilder_ExemplarDeduplication(t *testing.T) {
   135  	builder := NewTimeSeriesBuilder()
   136  	labels := Labels{
   137  		{Name: "service_name", Value: "api"},
   138  		{Name: "pod", Value: "pod-1"},
   139  	}
   140  
   141  	builder.Add(1, labels, 1000, 100.0, schemav1.Annotations{}, "profile-dup")
   142  	builder.Add(1, labels, 1000, 200.0, schemav1.Annotations{}, "profile-dup")
   143  
   144  	series := builder.BuildWithExemplars()
   145  	require.Len(t, series, 1)
   146  	require.Len(t, series[0].Points, 2)
   147  
   148  	// Should deduplicate to 1 exemplar per point
   149  	for _, point := range series[0].Points {
   150  		require.Len(t, point.Exemplars, 1)
   151  		assert.Equal(t, "profile-dup", point.Exemplars[0].ProfileId)
   152  	}
   153  }
   154  
   155  func TestExemplarBuilder_SameProfileIDDifferentValues(t *testing.T) {
   156  	builder := NewExemplarBuilder()
   157  
   158  	labels1 := Labels{
   159  		{Name: "pod", Value: "pod-1"},
   160  	}
   161  	labels2 := Labels{
   162  		{Name: "pod", Value: "pod-1"},
   163  		{Name: "span_name", Value: "POST"},
   164  	}
   165  
   166  	builder.Add(1, labels1, 1000, "profile-123", 12830000000)
   167  	builder.Add(2, labels2, 1000, "profile-123", 110000000)
   168  
   169  	exemplars := builder.Build()
   170  	require.Len(t, exemplars, 1)
   171  
   172  	exemplar := exemplars[0]
   173  	assert.Equal(t, "profile-123", exemplar.ProfileId)
   174  	assert.Equal(t, int64(1000), exemplar.Timestamp)
   175  	assert.Equal(t, uint64(12940000000), exemplar.Value)
   176  
   177  	// Labels should be intersected
   178  	assert.Len(t, exemplar.Labels, 1)
   179  	assert.Equal(t, "pod", exemplar.Labels[0].Name)
   180  	assert.Equal(t, "pod-1", exemplar.Labels[0].Value)
   181  }
   182  
   183  func TestExemplarBuilder_DifferentProfileIDsNotSummed(t *testing.T) {
   184  	builder := NewExemplarBuilder()
   185  
   186  	labels1 := Labels{
   187  		{Name: "pod", Value: "pod-1"},
   188  		{Name: "span_name", Value: "POST"},
   189  	}
   190  	labels2 := Labels{
   191  		{Name: "pod", Value: "pod-2"},
   192  		{Name: "span_name", Value: "POST"},
   193  	}
   194  
   195  	builder.Add(1, labels1, 1000, "profile-abc", 110000000)
   196  	builder.Add(2, labels2, 1000, "profile-def", 150000000)
   197  
   198  	exemplars := builder.Build()
   199  	require.Len(t, exemplars, 2)
   200  
   201  	// Sort by profile ID to ensure consistent ordering
   202  	sort.Slice(exemplars, func(i, j int) bool {
   203  		return exemplars[i].ProfileId < exemplars[j].ProfileId
   204  	})
   205  
   206  	// First exemplar
   207  	assert.Equal(t, "profile-abc", exemplars[0].ProfileId)
   208  	assert.Equal(t, uint64(110000000), exemplars[0].Value)
   209  
   210  	// Second exemplar
   211  	assert.Equal(t, "profile-def", exemplars[1].ProfileId)
   212  	assert.Equal(t, uint64(150000000), exemplars[1].Value)
   213  }
   214  
   215  func TestTimeSeriesBuilder_MultipleSeries(t *testing.T) {
   216  	builder := NewTimeSeriesBuilder("env")
   217  	labels1 := Labels{
   218  		{Name: "service_name", Value: "api"},
   219  		{Name: "env", Value: "prod"},
   220  	}
   221  	labels2 := Labels{
   222  		{Name: "service_name", Value: "api"},
   223  		{Name: "env", Value: "staging"},
   224  	}
   225  
   226  	builder.Add(1, labels1, 1000, 100.0, schemav1.Annotations{}, "prod-profile")
   227  	builder.Add(2, labels2, 1000, 200.0, schemav1.Annotations{}, "staging-profile")
   228  
   229  	series := builder.BuildWithExemplars()
   230  	require.Len(t, series, 2)
   231  
   232  	seriesByEnv := make(map[string]*typesv1.Series)
   233  	for _, s := range series {
   234  		for _, lp := range s.Labels {
   235  			if lp.Name == "env" {
   236  				seriesByEnv[lp.Value] = s
   237  				break
   238  			}
   239  		}
   240  	}
   241  
   242  	prodSeries := seriesByEnv["prod"]
   243  	require.NotNil(t, prodSeries)
   244  	require.Len(t, prodSeries.Points, 1)
   245  	require.Len(t, prodSeries.Points[0].Exemplars, 1)
   246  	assert.Equal(t, "prod-profile", prodSeries.Points[0].Exemplars[0].ProfileId)
   247  
   248  	stagingSeries := seriesByEnv["staging"]
   249  	require.NotNil(t, stagingSeries)
   250  	require.Len(t, stagingSeries.Points, 1)
   251  	require.Len(t, stagingSeries.Points[0].Exemplars, 1)
   252  	assert.Equal(t, "staging-profile", stagingSeries.Points[0].Exemplars[0].ProfileId)
   253  }
   254  
   255  func findLabelValue(labels []*typesv1.LabelPair, name string) string {
   256  	for _, lp := range labels {
   257  		if lp.Name == name {
   258  			return lp.Value
   259  		}
   260  	}
   261  	return ""
   262  }