github.com/matrixorigin/matrixone@v1.2.0/pkg/util/metric/mometric/stats_log_writer_test.go (about)

     1  // Copyright 2023 Matrix Origin
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package mometric
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"math/rand"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/matrixorigin/matrixone/pkg/common/moerr"
    26  	"github.com/matrixorigin/matrixone/pkg/common/runtime"
    27  	"github.com/matrixorigin/matrixone/pkg/logutil"
    28  	"github.com/matrixorigin/matrixone/pkg/objectio"
    29  	"github.com/matrixorigin/matrixone/pkg/pb/metadata"
    30  	"github.com/matrixorigin/matrixone/pkg/util/metric/stats"
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/stretchr/testify/require"
    33  	"go.uber.org/zap"
    34  	"go.uber.org/zap/zapcore"
    35  )
    36  
    37  // MockService for testing stats Registry
    38  type MockService struct {
    39  	stats *MockStats
    40  }
    41  
    42  type MockStats struct {
    43  	reads stats.Counter
    44  	hits  stats.Counter
    45  }
    46  
    47  func NewMockService() *MockService {
    48  	return &MockService{
    49  		stats: &MockStats{},
    50  	}
    51  }
    52  
    53  func (d *MockService) Do() {
    54  	d.stats.reads.Add(2)
    55  	d.stats.hits.Add(1)
    56  }
    57  
    58  func (d *MockService) Stats() *MockStats {
    59  	return d.stats
    60  }
    61  
    62  // LogExporter for the Mock Service declared above
    63  type MockServiceLogExporter struct {
    64  	service *MockService
    65  }
    66  
    67  func NewMockServiceLogExporter(service *MockService) stats.LogExporter {
    68  	return &MockServiceLogExporter{
    69  		service: service,
    70  	}
    71  }
    72  
    73  func (c *MockServiceLogExporter) Export() []zap.Field {
    74  	var fields []zap.Field
    75  
    76  	stats := c.service.Stats()
    77  
    78  	reads := stats.reads.SwapW(0)
    79  	hits := stats.hits.SwapW(0)
    80  
    81  	fields = append(fields, zap.Any("reads", reads))
    82  	fields = append(fields, zap.Any("hits", hits))
    83  
    84  	return fields
    85  }
    86  
    87  func TestStatsLogWriter(t *testing.T) {
    88  	// 1. Register Dev Stats
    89  	service := NewMockService()
    90  	serviceLogExporter := NewMockServiceLogExporter(service)
    91  	stats.Register("MockServiceStats", stats.WithLogExporter(serviceLogExporter))
    92  
    93  	//2.1 Setup a Runtime
    94  	runtime.SetupProcessLevelRuntime(runtime.NewRuntime(metadata.ServiceType_CN, "test", logutil.GetGlobalLogger()))
    95  
    96  	//2.2 Create custom Hook logger
    97  	type threadSafeWrittenLog struct {
    98  		content []zapcore.Entry
    99  		// This is because, BusyLoop and StatsLogger read (ie len) and write (content) to the same field.
   100  		sync.Mutex
   101  	}
   102  
   103  	writtenLogs := threadSafeWrittenLog{}
   104  	customLogger := runtime.ProcessLevelRuntime().Logger().WithOptions(zap.Hooks(func(entry zapcore.Entry) error {
   105  		writtenLogs.Lock()
   106  		defer writtenLogs.Unlock()
   107  		writtenLogs.content = append(writtenLogs.content, entry)
   108  		return nil
   109  	}))
   110  
   111  	// 2.3 Start the LogWriter
   112  	c := newStatsLogWriter(stats.DefaultRegistry, customLogger, 100*time.Millisecond)
   113  	serviceCtx := context.WithValue(context.Background(), ServiceTypeKey, "DN")
   114  	assert.True(t, c.Start(serviceCtx))
   115  
   116  	// 3. Perform operations on Dev Stats
   117  	service.Do()
   118  
   119  	// 4. Wait for log to print in console. (Busy Loop)
   120  	err := waitUtil(60*time.Second, 100*time.Millisecond, func() bool {
   121  		writtenLogs.Lock()
   122  		defer writtenLogs.Unlock()
   123  		return len(writtenLogs.content) >= 10
   124  	})
   125  	require.NoError(t, err)
   126  
   127  	// 5. Stop the LogWriter
   128  	if ch, effect := c.Stop(true); effect {
   129  		<-ch
   130  	}
   131  	println("StatsLogWriter has stopped gracefully.")
   132  
   133  	//6. Validate the log printed.
   134  	writtenLogs.Lock()
   135  	defer writtenLogs.Unlock()
   136  	assert.True(t, len(writtenLogs.content) >= 10)
   137  	for _, log := range writtenLogs.content {
   138  		assert.Contains(t, log.Message, "Stats")
   139  	}
   140  
   141  	// 7. (Optional) Read from the console and validate the log. Example log:
   142  	// 2023/03/15 02:37:31.767463 -0500 INFO cn-service mometric/stats_log_writer.go:86 MockServiceStats stats  {"uuid": "test", "reads": 2, "hits": 1}
   143  	// 2023/03/15 02:37:33.767659 -0500 INFO cn-service mometric/stats_log_writer.go:86 MockServiceStats stats  {"uuid": "test", "reads": 0, "hits": 0}
   144  	// 2023/03/15 02:37:35.767608 -0500 INFO cn-service mometric/stats_log_writer.go:86 MockServiceStats stats  {"uuid": "test", "reads": 0, "hits": 0}
   145  	// StatsLogWriter has stopped gracefully.
   146  
   147  }
   148  
   149  // waitUtil Busy Loop to wait until check() is true.
   150  func waitUtil(timeout, checkInterval time.Duration, check func() bool) error {
   151  	timeoutTimer := time.After(timeout)
   152  	for {
   153  		select {
   154  		case <-timeoutTimer:
   155  			return moerr.NewInternalError(context.TODO(), "timeout")
   156  		default:
   157  			if check() {
   158  				return nil
   159  			}
   160  			time.Sleep(checkInterval)
   161  		}
   162  	}
   163  }
   164  
   165  func TestWriteBlkReadStats(t *testing.T) {
   166  	h1, t1 := rand.Int(), rand.Int()+1
   167  	objectio.BlkReadStats.BlksByReaderStats.Record(h1, t1)
   168  
   169  	h2, t2 := rand.Int(), rand.Int()+1
   170  	objectio.BlkReadStats.EntryCacheHitStats.Record(h2, t2)
   171  
   172  	h3, t3 := rand.Int(), rand.Int()+1
   173  	objectio.BlkReadStats.BlkCacheHitStats.Record(h3, t3)
   174  
   175  	s := new(StatsLogWriter)
   176  	type threadSafeWrittenLog struct {
   177  		content []zapcore.Entry
   178  		// This is because, BusyLoop and StatsLogger read (ie len) and write (content) to the same field.
   179  		sync.Mutex
   180  	}
   181  
   182  	writtenLogs := threadSafeWrittenLog{}
   183  	runtime.SetupProcessLevelRuntime(runtime.NewRuntime(metadata.ServiceType_CN, "test", logutil.GetGlobalLogger()))
   184  	s.logger = runtime.ProcessLevelRuntime().Logger().WithOptions(zap.Hooks(func(entry zapcore.Entry) error {
   185  		writtenLogs.Lock()
   186  		defer writtenLogs.Unlock()
   187  		writtenLogs.content = append(writtenLogs.content, entry)
   188  		return nil
   189  	}))
   190  	s.writeBlkReadStats()
   191  
   192  	expected := fmt.Sprintf("duration: %d, "+
   193  		"blk hit rate: %d/%d=%.4f, entry hit rate: %d/%d=%.4f, (average) blks in each reader: %d/%d=%.4f",
   194  		s.gatherInterval,
   195  		h3, t3, float32(h3)/float32(t3),
   196  		h2, t2, float32(h2)/float32(t2),
   197  		t1, h1, float32(t1)/float32(h1),
   198  	)
   199  
   200  	require.Equal(t, writtenLogs.content[0].Level, zapcore.InfoLevel)
   201  	require.Equal(t, writtenLogs.content[0].Message, expected)
   202  }