github.com/grafana/pyroscope@v1.18.0/pkg/block/compaction_test.go (about)

     1  package block_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/prometheus/prometheus/model/labels"
    14  	"github.com/prometheus/prometheus/prompb"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/mock"
    17  	"github.com/stretchr/testify/require"
    18  	"google.golang.org/protobuf/encoding/protojson"
    19  
    20  	metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1"
    21  	"github.com/grafana/pyroscope/pkg/block"
    22  	"github.com/grafana/pyroscope/pkg/metrics"
    23  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    24  	"github.com/grafana/pyroscope/pkg/objstore/testutil"
    25  	"github.com/grafana/pyroscope/pkg/test/mocks/mockmetrics"
    26  )
    27  
    28  func Test_CompactBlocks(t *testing.T) {
    29  	ctx := context.Background()
    30  	bucket, _ := testutil.NewFilesystemBucket(t, ctx, "testdata")
    31  
    32  	var resp metastorev1.GetBlockMetadataResponse
    33  	raw, err := os.ReadFile("testdata/block-metas.json")
    34  	require.NoError(t, err)
    35  	err = protojson.Unmarshal(raw, &resp)
    36  	require.NoError(t, err)
    37  
    38  	dst, tempdir := testutil.NewFilesystemBucket(t, ctx, t.TempDir())
    39  	compactedBlocks, err := block.Compact(ctx, resp.Blocks, bucket,
    40  		block.WithCompactionDestination(dst),
    41  		block.WithCompactionTempDir(tempdir),
    42  		block.WithCompactionObjectOptions(
    43  			block.WithObjectDownload(filepath.Join(tempdir, "source")),
    44  			block.WithObjectMaxSizeLoadInMemory(0)), // Force download.
    45  	)
    46  
    47  	require.NoError(t, err)
    48  	require.Len(t, compactedBlocks, 1)
    49  	require.NotZero(t, compactedBlocks[0].Size)
    50  	require.Len(t, compactedBlocks[0].Datasets, 4)
    51  
    52  	compactedJson, err := json.MarshalIndent(compactedBlocks, "", "  ")
    53  	require.NoError(t, err)
    54  	expectedJson, err := os.ReadFile("testdata/compacted.golden")
    55  	require.NoError(t, err)
    56  	assert.Equal(t, string(expectedJson), string(compactedJson))
    57  
    58  	t.Run("Compact compacted blocks", func(t *testing.T) {
    59  		compactedBlocks, err = block.Compact(ctx, compactedBlocks, dst,
    60  			block.WithCompactionDestination(dst),
    61  			block.WithCompactionTempDir(tempdir),
    62  			block.WithCompactionObjectOptions(
    63  				block.WithObjectDownload(filepath.Join(tempdir, "source")),
    64  				block.WithObjectMaxSizeLoadInMemory(0)), // Force download.
    65  		)
    66  
    67  		require.NoError(t, err)
    68  		require.Len(t, compactedBlocks, 1)
    69  		require.NotZero(t, compactedBlocks[0].Size)
    70  		require.Len(t, compactedBlocks[0].Datasets, 4)
    71  	})
    72  }
    73  
    74  func Test_CompactBlocks_recordingRules(t *testing.T) {
    75  	ctx := context.Background()
    76  	bucket, _ := testutil.NewFilesystemBucket(t, ctx, "testdata")
    77  
    78  	var resp metastorev1.GetBlockMetadataResponse
    79  	raw, err := os.ReadFile("testdata/block-metas.json")
    80  	require.NoError(t, err)
    81  	err = protojson.Unmarshal(raw, &resp)
    82  	require.NoError(t, err)
    83  
    84  	exporter := &stringExporter{}
    85  	ruler := new(mockmetrics.MockRuler)
    86  	ruler.On("RecordingRules", mock.Anything).Return([]*phlaremodel.RecordingRule{
    87  		{
    88  			Matchers: []*labels.Matcher{
    89  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "goroutine:goroutine:count:goroutine:count"),
    90  			},
    91  			GroupBy:        []string{"service_name"},
    92  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_goroutines_total_count"}),
    93  		},
    94  		{
    95  			Matchers: []*labels.Matcher{
    96  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:alloc_objects:count:space:bytes"),
    97  			},
    98  			GroupBy:        []string{"service_name"},
    99  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_alloc_total_count"}),
   100  		},
   101  		{
   102  			Matchers: []*labels.Matcher{
   103  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:alloc_space:bytes:space:bytes"),
   104  			},
   105  			GroupBy:        []string{"service_name"},
   106  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_alloc_total_bytes"}),
   107  		},
   108  		{
   109  			Matchers: []*labels.Matcher{
   110  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:inuse_objects:count:space:bytes"),
   111  			},
   112  			GroupBy:        []string{"service_name"},
   113  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_inuse_total_count"}),
   114  		},
   115  		{
   116  			Matchers: []*labels.Matcher{
   117  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:inuse_space:bytes:space:bytes"),
   118  			},
   119  			GroupBy:        []string{"service_name"},
   120  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_inuse_total_bytes"}),
   121  		},
   122  		{
   123  			Matchers: []*labels.Matcher{
   124  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   125  			},
   126  			GroupBy:        []string{"service_name"},
   127  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_cpu_usage_total_nanoseconds"}),
   128  		},
   129  		{
   130  			Matchers: []*labels.Matcher{
   131  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "process_cpu:samples:count:cpu:nanoseconds"),
   132  			},
   133  			GroupBy:        []string{"service_name"},
   134  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_cpu_usage_total_samples"}),
   135  		},
   136  		// functions
   137  		{
   138  			Matchers: []*labels.Matcher{
   139  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "goroutine:goroutine:count:goroutine:count"),
   140  			},
   141  			GroupBy:        []string{"service_name"},
   142  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_goroutines_function_total_servehttp_count"}),
   143  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   144  		},
   145  		{
   146  			Matchers: []*labels.Matcher{
   147  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:alloc_objects:count:space:bytes"),
   148  			},
   149  			GroupBy:        []string{"service_name"},
   150  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_alloc_function_total_servehttp_count"}),
   151  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   152  		},
   153  		{
   154  			Matchers: []*labels.Matcher{
   155  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:alloc_space:bytes:space:bytes"),
   156  			},
   157  			GroupBy:        []string{"service_name"},
   158  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_alloc_function_total_servehttp_bytes"}),
   159  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   160  		},
   161  		{
   162  			Matchers: []*labels.Matcher{
   163  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:inuse_objects:count:space:bytes"),
   164  			},
   165  			GroupBy:        []string{"service_name"},
   166  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_inuse_function_total_servehttp_count"}),
   167  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   168  		},
   169  		{
   170  			Matchers: []*labels.Matcher{
   171  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:inuse_space:bytes:space:bytes"),
   172  			},
   173  			GroupBy:        []string{"service_name"},
   174  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_inuse_function_total_servehttp_bytes"}),
   175  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   176  		},
   177  		{
   178  			Matchers: []*labels.Matcher{
   179  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "process_cpu:cpu:nanoseconds:cpu:nanoseconds"),
   180  			},
   181  			GroupBy:        []string{"service_name"},
   182  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_cpu_usage_function_total_servehttp_nanoseconds"}),
   183  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   184  		},
   185  		{
   186  			Matchers: []*labels.Matcher{
   187  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "process_cpu:samples:count:cpu:nanoseconds"),
   188  			},
   189  			GroupBy:        []string{"service_name"},
   190  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_cpu_usage_function_total_servehttp_samples"}),
   191  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   192  		},
   193  	})
   194  	sampleObserver := metrics.NewSampleObserver(0, exporter, ruler, labels.EmptyLabels())
   195  
   196  	compactedBlocks, err := block.Compact(ctx, resp.Blocks, bucket,
   197  		block.WithSampleObserver(sampleObserver),
   198  	)
   199  	// Close observer to flush export
   200  	sampleObserver.Close()
   201  
   202  	require.NoError(t, err)
   203  	require.Len(t, compactedBlocks, 1)
   204  	require.NotZero(t, compactedBlocks[0].Size)
   205  	require.Len(t, compactedBlocks[0].Datasets, 4)
   206  
   207  	expectedMetrics, err := os.ReadFile("testdata/profiles_recorded.txt")
   208  	require.NoError(t, err)
   209  	expectedMetricsArray := strings.Split(string(expectedMetrics), "\n")
   210  	sort.Strings(expectedMetricsArray)
   211  	actualMetricsArray := strings.Split(exporter.String(), "\n")
   212  	sort.Strings(actualMetricsArray)
   213  	assert.Equal(t, expectedMetricsArray, actualMetricsArray)
   214  
   215  	compactedJson, err := json.MarshalIndent(compactedBlocks, "", "  ")
   216  	require.NoError(t, err)
   217  	expectedJson, err := os.ReadFile("testdata/compacted.golden")
   218  	require.NoError(t, err)
   219  	assert.Equal(t, string(expectedJson), string(compactedJson))
   220  }
   221  
   222  func Test_CompactBlocks_recordingRules_shadowedSymbols(t *testing.T) {
   223  	ctx := context.Background()
   224  	bucket, _ := testutil.NewFilesystemBucket(t, ctx, "testdata")
   225  
   226  	var resp metastorev1.GetBlockMetadataResponse
   227  	raw, err := os.ReadFile("testdata/block-metas.json")
   228  	require.NoError(t, err)
   229  	err = protojson.Unmarshal(raw, &resp)
   230  	require.NoError(t, err)
   231  
   232  	exporter := &stringExporter{}
   233  	ruler := new(mockmetrics.MockRuler)
   234  	ruler.On("RecordingRules", mock.Anything).Return([]*phlaremodel.RecordingRule{
   235  		{
   236  			Matchers: []*labels.Matcher{
   237  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/ingester"),
   238  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:alloc_space:bytes:space:bytes"),
   239  			},
   240  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_alloc_total_pyroscope_ingester_Push_bytes"}),
   241  			FunctionName:   "github.com/grafana/pyroscope/pkg/ingester.(*Ingester).Push",
   242  		},
   243  		{
   244  			Matchers: []*labels.Matcher{
   245  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/ingester"),
   246  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:inuse_space:bytes:space:bytes"),
   247  			},
   248  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_inuse_total_pyroscope_ingester_Push_bytes"}),
   249  			FunctionName:   "github.com/grafana/pyroscope/pkg/ingester.(*Ingester).Push",
   250  		},
   251  		{
   252  			Matchers: []*labels.Matcher{
   253  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/ingester"),
   254  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "process_cpu:samples:count:cpu:nanoseconds"),
   255  			},
   256  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_cpu_usage_total_pyroscope_ingester_Push_samples"}),
   257  			FunctionName:   "github.com/grafana/pyroscope/pkg/ingester.(*Ingester).Push",
   258  		},
   259  		{
   260  			Matchers: []*labels.Matcher{
   261  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/ingester"),
   262  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:alloc_space:bytes:space:bytes"),
   263  			},
   264  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_alloc_total_pyroscope_ingester_Serve_bytes"}),
   265  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   266  		},
   267  		{
   268  			Matchers: []*labels.Matcher{
   269  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/ingester"),
   270  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:inuse_space:bytes:space:bytes"),
   271  			},
   272  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_inuse_total_pyroscope_ingester_Serve_bytes"}),
   273  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   274  		},
   275  		{
   276  			Matchers: []*labels.Matcher{
   277  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/ingester"),
   278  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "process_cpu:samples:count:cpu:nanoseconds"),
   279  			},
   280  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_cpu_usage_total_pyroscope_ingester_Serve_samples"}),
   281  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   282  		},
   283  		{
   284  			Matchers: []*labels.Matcher{
   285  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/query-frontend"),
   286  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:inuse_space:bytes:space:bytes"),
   287  			},
   288  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_inuse_total_query_Serve_bytes"}),
   289  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   290  		},
   291  		{
   292  			Matchers: []*labels.Matcher{
   293  				labels.MustNewMatcher(labels.MatchEqual, "service_name", "pyroscope-test/query-frontend"),
   294  				labels.MustNewMatcher(labels.MatchEqual, "__profile_type__", "memory:alloc_space:bytes:space:bytes"),
   295  			},
   296  			ExternalLabels: labels.New(labels.Label{Name: "__name__", Value: "profiles_recorded_mem_alloc_total_query_Serve_bytes"}),
   297  			FunctionName:   "net/http.HandlerFunc.ServeHTTP",
   298  		},
   299  	})
   300  	sampleObserver := metrics.NewSampleObserver(0, exporter, ruler, labels.EmptyLabels())
   301  
   302  	compactedBlocks, err := block.Compact(ctx, resp.Blocks, bucket,
   303  		block.WithSampleObserver(sampleObserver),
   304  	)
   305  	// Close observer to flush export
   306  	sampleObserver.Close()
   307  
   308  	require.NoError(t, err)
   309  	require.Len(t, compactedBlocks, 1)
   310  	require.NotZero(t, compactedBlocks[0].Size)
   311  	require.Len(t, compactedBlocks[0].Datasets, 4)
   312  
   313  	expectedMetrics, err := os.ReadFile("testdata/profiles_recorded_shadowed.txt")
   314  	require.NoError(t, err)
   315  	expectedMetricsArray := strings.Split(string(expectedMetrics), "\n")
   316  	sort.Strings(expectedMetricsArray)
   317  	actualMetricsArray := strings.Split(exporter.String(), "\n")
   318  	sort.Strings(actualMetricsArray)
   319  	assert.Equal(t, expectedMetricsArray, actualMetricsArray)
   320  
   321  	compactedJson, err := json.MarshalIndent(compactedBlocks, "", "  ")
   322  	require.NoError(t, err)
   323  	expectedJson, err := os.ReadFile("testdata/compacted.golden")
   324  	require.NoError(t, err)
   325  	assert.Equal(t, string(expectedJson), string(compactedJson))
   326  }
   327  
   328  type stringExporter struct {
   329  	output string
   330  }
   331  
   332  func (e *stringExporter) Send(tenant string, series []prompb.TimeSeries) error {
   333  	for _, s := range series {
   334  		e.output += fmt.Sprintf("%s: %s\n", tenant, s.String())
   335  	}
   336  	return nil
   337  }
   338  
   339  func (*stringExporter) Flush() {}
   340  
   341  func (e *stringExporter) String() string {
   342  	return e.output
   343  }