11package io .lettuce .test .metrics ;
22
3+ import io .lettuce .core .LettuceVersion ;
4+ import io .lettuce .test .config .TestRunProperties ;
35import io .lettuce .test .workloads .BaseWorkload ;
46import io .micrometer .core .instrument .Counter ;
57import io .micrometer .core .instrument .Gauge ;
1416import org .springframework .beans .factory .annotation .Value ;
1517import org .springframework .scheduling .TaskScheduler ;
1618import org .springframework .stereotype .Component ;
19+ import com .fasterxml .jackson .databind .ObjectMapper ;
20+ import com .fasterxml .jackson .databind .node .ObjectNode ;
1721
22+ import java .io .IOException ;
1823import java .lang .reflect .Proxy ;
24+ import java .nio .file .Files ;
25+ import java .nio .file .Path ;
26+ import java .nio .file .Paths ;
1927import java .time .Duration ;
28+ import java .time .Instant ;
2029import java .util .Comparator ;
2130import java .util .Map ;
31+ import java .util .OptionalDouble ;
2232import java .util .concurrent .ConcurrentHashMap ;
2333import java .util .concurrent .ScheduledFuture ;
2434import java .util .concurrent .TimeUnit ;
@@ -28,15 +38,31 @@ public class MetricsReporter {
2838
2939 private static final Logger log = LoggerFactory .getLogger ("simple-metrics-reporter" );
3040
41+ public static final String REDIS_OPERATION_DURATION_TOTAL = "redis.operation.duration.total" ;
42+
43+ public static final String REDIS_OPERATION_DURATION = "redis.operation.duration" ;
44+
45+ public static final String REDIS_RECONNECTION_DURATION = "redis.reconnection.duration" ;
46+
47+ public static final String REDIS_TEST_DURATION = "redis.test.duration" ;
48+
3149 private final MeterRegistry meterRegistry ;
3250
3351 private final SimpleMeterRegistry simpleMeterRegistry ;
3452
3553 private final TaskScheduler taskScheduler ;
3654
55+ private final TestRunProperties testRunProperties ;
56+
3757 @ Value ("${simple.metrics.dumpRate:PT5S}" )
3858 private Duration dumpRate ;
3959
60+ @ Value ("${logging.file.path:logs}" )
61+ private String logPath ;
62+
63+ @ Value ("${runner.test.workload.type}" )
64+ private String workloadType ;
65+
4066 private final Map <CommandKey , Timer > commandLatencyTimers = new ConcurrentHashMap <>();
4167
4268 private final Timer commandLatencyTotalTimer ;
@@ -67,10 +93,17 @@ public class MetricsReporter {
6793
6894 private ScheduledFuture <?> scheduledFuture ;
6995
96+ private final ObjectMapper objectMapper = new ObjectMapper ();
97+
98+ private volatile Instant testRunStart ;
99+
100+ private volatile Instant testRunEnd ;
101+
70102 public MetricsReporter (MeterRegistry meterRegistry , SimpleMeterRegistry simpleMeterRegistry ,
71- @ Qualifier ("metricReporterScheduler" ) TaskScheduler taskScheduler ) {
103+ @ Qualifier ("metricReporterScheduler" ) TaskScheduler taskScheduler , TestRunProperties testRunProperties ) {
72104 this .meterRegistry = meterRegistry ;
73105 this .simpleMeterRegistry = simpleMeterRegistry ;
106+ this .testRunProperties = testRunProperties ;
74107 this .connectionSuccessTimer = Timer .builder ("lettuce.connect.success" )
75108 .description ("Measures the duration and count of successful Redis connections" ).register (meterRegistry );
76109 this .connectionFailureTimer = Timer .builder ("lettuce.connect.failure" )
@@ -95,6 +128,15 @@ Timer.Sample startCommandTimer() {
95128 return Timer .start (meterRegistry );
96129 }
97130
131+ public void recordTestDuration (Instant start , Instant end ) {
132+ this .testRunStart = start ;
133+ this .testRunEnd = end ;
134+ Timer .builder (REDIS_TEST_DURATION )
135+ .description ("Measures the duration of the test run" )
136+ .register (meterRegistry )
137+ .record (Duration .between (start , end ));
138+ }
139+
98140 public record CommandKey (String commandName , OperationStatus status ) {
99141
100142 }
@@ -167,17 +209,19 @@ private Counter createPubSubOperationCounter(PubSubOpKey pubSubOpKey) {
167209 .register (meterRegistry );
168210 }
169211
170- private Timer createCommandLatencyTimer (CommandKey commandKey ) {
171- return Timer .builder ("redis.operation.duration" ).description (
212+ private Timer createCommandLatencyTimer (CommandKey commandKey ) {
213+ return Timer .builder (REDIS_OPERATION_DURATION ).description (
172214 "Measures the execution time of Redis commands from API invocation until command completion per command" )
173215 .tag ("command" , commandKey .commandName ).tag ("status" , commandKey .status .name ().toLowerCase ())
174216 .publishPercentileHistogram (true ).publishPercentiles (0.5 , 0.95 , 0.99 ).register (meterRegistry );
175217 }
176218
177219 private Timer createCommandLatencyTotalTimer () {
178- return Timer .builder ("redis.operation.total.duration" )
220+ return Timer .builder (REDIS_OPERATION_DURATION_TOTAL )
179221 .description ("Measures the execution time of Redis commands from API invocation until command completion" )
180- .publishPercentileHistogram (true ).publishPercentiles (0.5 , 0.95 , 0.99 ).register (meterRegistry );
222+ .publishPercentileHistogram (true )
223+ .publishPercentiles (0.5 , 0.95 , 0.99 )
224+ .register (meterRegistry );
181225 }
182226
183227 private Counter createCommandErrorCounter (String commandName ) {
@@ -273,18 +317,149 @@ public void dumpMetrics() {
273317 @ PostConstruct
274318 public void startScheduledTask () {
275319 scheduledFuture = taskScheduler .scheduleAtFixedRate (this ::dumpMetrics , dumpRate );
320+ taskScheduler .scheduleAtFixedRate (this ::dumpFinalResult , dumpRate );
276321 }
277322
278323 @ PreDestroy
279324 public void shutdown () {
280325 log .info ("MetricsReporter is shutting down." );
326+ dumpFinalResult ();
281327 if (scheduledFuture != null ) {
282328 scheduledFuture .cancel (true );
283329 dumpMetrics ();
284330 log .info ("MetricsReporter is stopped." );
285331 }
286332 }
287333
334+ public void dumpFinalResult () {
335+
336+ try {
337+ ObjectNode result = buildFinalResultJson ();
338+ String jsonResult = objectMapper .writerWithDefaultPrettyPrinter ().writeValueAsString (result );
339+
340+ // Log to console
341+ log .info ("=== FINAL TEST RESULTS ===" );
342+ log .info (jsonResult );
343+ log .info ("=== END FINAL TEST RESULTS ===" );
344+
345+ // Write to file
346+ writeResultsToFile (jsonResult );
347+
348+ } catch (Exception e ) {
349+ log .error ("Error generating final result" , e );
350+ }
351+ }
352+
353+ private void writeResultsToFile (String jsonResult ) {
354+ try {
355+ // Create logs directory if it doesn't exist
356+ Path logDir = Paths .get (logPath );
357+ if (!Files .exists (logDir )) {
358+ Files .createDirectories (logDir );
359+ log .info ("Created log directory: {}" , logDir .toAbsolutePath ());
360+ }
361+
362+ // Write to test-run-summary.json
363+ Path summaryFile = logDir .resolve ("test-run-summary.json" );
364+ Files .write (summaryFile , jsonResult .getBytes ());
365+ log .info ("Final test results written to: {}" , summaryFile .toAbsolutePath ());
366+
367+ } catch (IOException e ) {
368+ log .error ("Failed to write final results to file" , e );
369+ }
370+ }
371+
372+ public String getFinalResultAsJson () {
373+ try {
374+ ObjectNode result = buildFinalResultJson ();
375+ return objectMapper .writerWithDefaultPrettyPrinter ().writeValueAsString (result );
376+ } catch (Exception e ) {
377+ log .error ("Error generating final result JSON" , e );
378+ return "{}" ;
379+ }
380+ }
381+
382+ private ObjectNode buildFinalResultJson () {
383+ ObjectNode result = objectMapper .createObjectNode ();
384+
385+ // Basic run information
386+ result .put ("app_name" , testRunProperties .getAppName ());
387+ result .put ("instance_id" , testRunProperties .getInstanceId ());
388+ result .put ("run_id" , testRunProperties .getRunId ());
389+ result .put ("version" , LettuceVersion .getVersion ());
390+
391+ // Test duration
392+ result .put ("workload_name" , workloadType != null ? workloadType : "unknown" );
393+
394+ // Command counts and success rate
395+ long totalCommands = getTotalCmdCount ();
396+ long successfulCommands = getSuccessfulCommandCount ();
397+ long failedCommands = getFailedCommandCount ();
398+
399+ result .put ("total_commands_count" , totalCommands );
400+ result .put ("successful_commands_count" , successfulCommands );
401+ result .put ("failed_commands_count" , failedCommands );
402+ result .put ("success_rate" ,
403+ String .format ("%.2f%%" , totalCommands > 0 ? (successfulCommands * 100.0 / totalCommands ) : 0.0 ));
404+
405+ // Reconnection metrics
406+ result .put ("avg_reconnection_duration_ms" , getAverageReconnectionDuration ().orElseGet (() -> 0.0 ));
407+
408+ // Run timestamps (as epoch seconds with decimals)
409+ result .put ("run_start" , testRunStart .getEpochSecond ());
410+ result .put ("run_end" , testRunEnd .getEpochSecond ());
411+
412+ // Latency statistics
413+ result .put ("min_latency_ms" , 0 );
414+ result .put ("max_latency_ms" , getMaxLatency ().orElse (0.0 ));
415+ result .put ("median_latency_ms" , getMedianLatency ().orElse (0.0 ));
416+ result .put ("p95_latency_ms" , getP95Latency ().orElse (0.0 ));
417+ result .put ("p99_latency_ms" , getP99 ().orElse (0.0 ));
418+
419+ return result ;
420+ }
421+
422+ private OptionalDouble getMedianLatency () {
423+ return simpleMeterRegistry .find (REDIS_OPERATION_DURATION_TOTAL ).timers ().stream ().mapToDouble ( m -> m .percentile (0.5 , TimeUnit .MILLISECONDS )).average ();
424+ }
425+
426+ private OptionalDouble getP95Latency () {
427+ return simpleMeterRegistry .find (REDIS_OPERATION_DURATION_TOTAL ).timers ().stream ().mapToDouble ( m -> m .percentile (0.95 , TimeUnit .MILLISECONDS )).average ();
428+ }
429+
430+ private OptionalDouble getP99 () {
431+ return simpleMeterRegistry .find (REDIS_OPERATION_DURATION_TOTAL ).timers ().stream ().mapToDouble ( m -> m .percentile (0.99 , TimeUnit .MILLISECONDS )).average ();
432+ }
433+
434+ private OptionalDouble getAverageReconnectionDuration () {
435+ return simpleMeterRegistry .find (REDIS_RECONNECTION_DURATION ).timers ().stream ().mapToDouble ( m -> m .mean (TimeUnit .MILLISECONDS )).average ();
436+
437+ }
438+
439+ private OptionalDouble getMaxLatency () {
440+ return simpleMeterRegistry .find (REDIS_OPERATION_DURATION ).timers ().stream ().mapToDouble ( m -> m .max (TimeUnit .MILLISECONDS )).max ();
441+ }
442+
443+ private long getTotalCmdCount () {
444+ return simpleMeterRegistry .find (REDIS_OPERATION_DURATION ).timers ().stream ().mapToLong (Timer ::count ).sum ();
445+ }
446+
447+ private long getSuccessfulCommandCount () {
448+ return simpleMeterRegistry .find (REDIS_OPERATION_DURATION )
449+ .tag ("status" , "success" )
450+ .timers ()
451+ .stream ()
452+ .mapToLong (Timer ::count ).sum ();
453+ }
454+
455+ private long getFailedCommandCount () {
456+ return simpleMeterRegistry .find (REDIS_OPERATION_DURATION )
457+ .tag ("status" , "error" )
458+ .timers ()
459+ .stream ()
460+ .mapToLong (Timer ::count ).sum ();
461+ }
462+
288463 public static CommandKey cmdKeyOk (String commandName ) {
289464 return new CommandKey (commandName , OperationStatus .SUCCESS );
290465 }
0 commit comments