QueryMetricsTextWriter.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.cosmos.implementation.query.metrics;

import com.azure.cosmos.implementation.apachecommons.lang.StringUtils;

import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.UUID;

public class QueryMetricsTextWriter extends QueryMetricsWriter {

    private final StringBuilder stringBuilder;

    // QueryMetrics
    private static final String ActivityIds = "Activity Ids";
    private static final String RETRIEVED_DOCUMENT_COUNT = "Retrieved Document Count";
    private static final String RETRIEVED_DOCUMENT_SIZE = "Retrieved Document Size";
    private static final String OUTPUT_DOCUMENT_COUNT = "Output Document Count";
    private static final String OUTPUT_DOCUMENT_SIZE = "Output Document Size";
    private static final String INDEX_UTILIZATION_TEXT = "Index Utilization";
    private static final String TOTAL_QUERY_EXECUTION_TIME = "Total Query Execution Time";

    // QueryPreparationTimes
    private static final String QUERY_PREPARATION_TIMES = "Query Preparation Times";
    private static final String QUERY_COMPILATION_TIME = "Query Compilation Time";
    private static final String LOGICAL_PLAN_BUILD_TIME = "Logical Plan Build Time";
    private static final String PHYSICAL_PLAN_BUILD_TIME = "Physical Plan Build Time";
    private static final String QUERY_OPTIMIZATION_TIME = "Query Optimization Time";

    // QueryTimes
    private static final String QUERY_ENGINE_TIMES = "Query Engine Times";
    private static final String INDEX_LOOKUP_TIME = "Index Lookup Time";
    private static final String DOCUMENT_LOAD_TIME = "Document Load Time";
    private static final String DOCUMENT_WRITE_TIME = "Document Write Time";

    // RuntimeExecutionTimes
    private static final String RUNTIME_EXECUTION_TIMES = "Runtime Execution Times";
    private static final String TOTAL_EXECUTION_TIME = "Query Engine Execution Time";
    private static final String SYSTEM_FUNCTION_EXECUTION_TIME = "System Function Execution Time";
    private static final String USER_DEFINED_FUNCTION_EXECUTION_TIME = "User-defined Function Execution Time";

    // ClientSideQueryMetrics
    private static final String CLIENT_SIDE_METRICS = "Client Side Metrics";
    private static final String RETRIES = "Retry Count";
    private static final String REQUEST_CHARGE = "Request Charge";
    private static final String FETCH_EXECUTION_RANGES = "Partition Execution Timeline";
    private static final String SCHEDULING_METRICS = "Scheduling Metrics";

    // Constants for Partition Execution Timeline Table
    public static final String START_TIME_HEADER = "Start Time (UTC)";
    public static final String END_TIME_HEADER = "End Time (UTC)";
    public static final String DURATION_HEADER = "Duration (ms)";
    private static final String PARTITION_KEY_RANGE_HEADER = "Partition Key Range";
    private static final String NUMBER_OF_DOCUMENTS_HEADER = "NUMBER of Documents";
    private static final String RETRY_COUNT_HEADER = "Retry Count";
    private static final String ACTIVITY_ID_HEADER = "Activity Id";

    // Constants for Scheduling Metrics Table
    private static final String PARTITION_RANGE_HEADER = "Partition Range";
    private static final String RESPONSE_TIME_HEADER = "Response Time (ms)";
    private static final String RUN_TIME_HEADER = "Run Time (ms)";
    private static final String WAIT_TIME_HEADER = "Wait Time (ms)";
    private static final String TURNAROUND_TIME_HEADER = "Turnaround Time (ms)";
    private static final String NUMBER_OF_PREEMPTION_HEADER = "NUMBER of Preemptions";

    // IndexUtilizationInfo
    private static final String INDEX_UTILIZATION_INFO_METRICS = "Index Utilization Info Metrics";
    private static final String UTILIZED_SINGLE_INDEXES_METRICS = " Utilized Single Indexes Metrics";
    private static final String POTENTIAL_SINGLE_INDEXES_METRICS = "Potential Single Indexes Metrics";
    private static final String UTILIZED_COMPOSITE_INDEXES_METRICS = "Utilized Composite Indexes Metrics";
    private static final String POTENTIAL_COMPOSITE_INDEXES_METRICS = "Potential Composite Indexes Metrics";

    // Constants for Utilized and Potential Single Indexes Table
    private static final String FILTER_EXPRESSION_HEADER = "Filter Expression";
    private static final String INDEX_DOCUMENT_EXPRESSION_HEADER = "Index Document Expression";
    private static final String FILTER_EXPRESSION_PRECISION_HEADER = "Filter Expression Precision";
    private static final String INDEX_PLAN_FULL_FIDELITY_HEADER = "Index Plan Full Fidelity";
    private static final String INDEX_IMPACT_SCORE_HEADER = "Index Impact Score";

    // Constants for Utilized and Potential Composite Indexes Table
    private static final String INDEX_DOCUMENT_EXPRESSIONS_HEADER = "Index Document Expressions";

    // Static  for Partition Execution Timeline Table
    // private static  int MaxDateTimeStringLength = LocalDateTime.MAX.toString().length();
    private static final int MAX_DATE_TIME_STRING_LENGTH = 16;
    private static final int START_TIME_HEADER_LENGTH
        = Math.max(MAX_DATE_TIME_STRING_LENGTH, START_TIME_HEADER.length());
    private static final int END_TIME_HEADER_LENGTH = Math.max(MAX_DATE_TIME_STRING_LENGTH, END_TIME_HEADER.length());
    private static final int DURATION_HEADER_LENGTH = DURATION_HEADER.length();
    private static final int PARTITION_KEY_RANGE_ID_HEADER_LENGTH = PARTITION_KEY_RANGE_HEADER.length();
    private static final int NUMBER_OF_DOCUMENTS_HEADER_LENGTH = NUMBER_OF_DOCUMENTS_HEADER.length();
    private static final int RETRY_COUNT_HEADER_LENGTH = RETRY_COUNT_HEADER.length();
    private static final int ACTIVITY_ID_HEADER_LENGTH = UUID.randomUUID().toString().length();

    private static final TextTable.Column[] PARTITION_EXECUTION_TIMELINE_COLUMNS = new TextTable.Column[]
            {
                    new TextTable.Column(PARTITION_KEY_RANGE_HEADER, PARTITION_KEY_RANGE_ID_HEADER_LENGTH),
                    new TextTable.Column(ACTIVITY_ID_HEADER, ACTIVITY_ID_HEADER_LENGTH),
                    new TextTable.Column(START_TIME_HEADER, START_TIME_HEADER_LENGTH),
                    new TextTable.Column(END_TIME_HEADER, END_TIME_HEADER_LENGTH),
                    new TextTable.Column(DURATION_HEADER, DURATION_HEADER_LENGTH),
                    new TextTable.Column(NUMBER_OF_DOCUMENTS_HEADER, NUMBER_OF_DOCUMENTS_HEADER_LENGTH),
                    new TextTable.Column(RETRY_COUNT_HEADER, RETRY_COUNT_HEADER_LENGTH),
            };

    private static final TextTable PARTITION_EXECUTION_TIMELINE_TABLE
        = new TextTable(Arrays.asList(PARTITION_EXECUTION_TIMELINE_COLUMNS));

    // Static  for Scheduling Metrics Table
    //private static readonly int MaxTimeSpanStringLength = Math.Max(TimeSpan.MaxValue.TotalMilliseconds.ToString
    // ("G17").Length, TurnaroundTimeHeader.Length);
    private static final int PARTITION_ID_HEADER_LENGTH = PARTITION_RANGE_HEADER.length();
    private static final int RESPONSE_TIME_HEADER_LENGTH = RESPONSE_TIME_HEADER.length();
    private static final int RUN_TIME_HEADER_LENGTH = RUN_TIME_HEADER.length();
    private static final int WAIT_TIME_HEADER_LENGTH = WAIT_TIME_HEADER.length();
    private static final int TURNAROUND_TIME_HEADER_LENGTH = TURNAROUND_TIME_HEADER.length();
    private static final int NUMBER_OF_PREEMPTION_HEADER_LENGTH = NUMBER_OF_PREEMPTION_HEADER.length();

    private static final TextTable.Column[] SCHEDULING_METRICS_COLUMNS = new TextTable.Column[]
            {
                    new TextTable.Column(PARTITION_RANGE_HEADER, PARTITION_ID_HEADER_LENGTH),
                    new TextTable.Column(RESPONSE_TIME_HEADER, RESPONSE_TIME_HEADER_LENGTH),
                    new TextTable.Column(RUN_TIME_HEADER, RUN_TIME_HEADER_LENGTH),
                    new TextTable.Column(WAIT_TIME_HEADER, WAIT_TIME_HEADER_LENGTH),
                    new TextTable.Column(TURNAROUND_TIME_HEADER, TURNAROUND_TIME_HEADER_LENGTH),
                    new TextTable.Column(NUMBER_OF_PREEMPTION_HEADER, NUMBER_OF_PREEMPTION_HEADER_LENGTH),
            };

    private static final TextTable SCHEDULING_METRICS_TABLE = new TextTable(Arrays.asList(SCHEDULING_METRICS_COLUMNS));

    // Static for Utilized and Potential Single Indexes Table
    private static final int FILTER_EXPRESSION_HEADER_LENGTH = FILTER_EXPRESSION_HEADER.length();
    private static final int INDEX_DOCUMENT_EXPRESSION_HEADER_LENGTH = INDEX_DOCUMENT_EXPRESSION_HEADER.length();
    private static final int FILTER_EXPRESSION_PRECISION_HEADER_LENGTH = FILTER_EXPRESSION_PRECISION_HEADER.length();
    private static final int INDEX_PLAN_FULL_FIDELITY_HEADER_LENGTH = INDEX_PLAN_FULL_FIDELITY_HEADER.length();
    private static final int INDEX_IMPACT_SCORE_HEADER_LENGTH = INDEX_IMPACT_SCORE_HEADER.length();

    private static final TextTable.Column[] UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_COLUMNS = new TextTable.Column[]
        {
            new TextTable.Column(FILTER_EXPRESSION_HEADER, FILTER_EXPRESSION_HEADER_LENGTH),
            new TextTable.Column(INDEX_DOCUMENT_EXPRESSION_HEADER, INDEX_DOCUMENT_EXPRESSION_HEADER_LENGTH),
            new TextTable.Column(FILTER_EXPRESSION_PRECISION_HEADER, FILTER_EXPRESSION_PRECISION_HEADER_LENGTH),
            new TextTable.Column(INDEX_PLAN_FULL_FIDELITY_HEADER, INDEX_PLAN_FULL_FIDELITY_HEADER_LENGTH),
            new TextTable.Column(INDEX_IMPACT_SCORE_HEADER, INDEX_IMPACT_SCORE_HEADER_LENGTH),
        };

    private static final TextTable UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE = new TextTable(Arrays.asList(UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_COLUMNS));

    // Static for Utilized and Potential Composite Indexes Table
    private static final int INDEX_DOCUMENT_EXPRESSIONS_HEADER_LENGTH = INDEX_DOCUMENT_EXPRESSIONS_HEADER.length();

    private static final TextTable.Column[] UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_COLUMNS = new TextTable.Column[]
        {
            new TextTable.Column(INDEX_DOCUMENT_EXPRESSIONS_HEADER, INDEX_DOCUMENT_EXPRESSIONS_HEADER_LENGTH),
            new TextTable.Column(INDEX_PLAN_FULL_FIDELITY_HEADER, INDEX_PLAN_FULL_FIDELITY_HEADER_LENGTH),
            new TextTable.Column(INDEX_IMPACT_SCORE_HEADER, INDEX_IMPACT_SCORE_HEADER_LENGTH),
        };

    private static final TextTable UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE = new TextTable(Arrays.asList(UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_COLUMNS));

    // FetchExecutionRange state
    private String lastFetchPartitionId;
    private String lastActivityId;
    private Instant lastStartTime;
    private Instant lastEndTime;
    private long lastFetchDocumentCount;
    private long lastFetchRetryCount;

    // PartitionSchedulingTimeSpan state
    private String lastSchedulingPartitionId;
    private long lastResponseTime;
    private long lastRunTime;
    private long lastWaitTime;
    private long lastTurnaroundTime;
    private long lastNumberOfPreemptions;

    // UtilizedSingleIndexes state
    private String lastUtilizedSingleFilterExpression;
    private String lastUtilizedSingleIndexDocumentExpression;
    private boolean lastUtilizedSingleFilterExpressionPrecision;
    private boolean lastUtilizedSingleIndexPlanFullFidelity;
    private String lastUtilizedSingleIndexImpactScore;

    // PotentialSingleIndexes state
    private String lastPotentialSingleFilterExpression;
    private String lastPotentialSingleIndexDocumentExpression;
    private boolean lastPotentialSingleFilterExpressionPrecision;
    private boolean lastPotentialSingleIndexPlanFullFidelity;
    private String lastPotentialSingleIndexImpactScore;

    // UtilizedCompositeIndexes state
    private List<String> lastUtilizedCompositeIndexDocumentExpressions;
    private boolean lastUtilizedCompositeIndexPlanFullFidelity;
    private String lastUtilizedCompositeIndexImpactScore;

    // PotentialCompositeIndexes state
    private List<String> lastPotentialCompositeIndexDocumentExpressions;
    private boolean lastPotentialCompositeIndexPlanFullFidelity;
    private String lastPotentialCompositeIndexImpactScore;

    public static final DateTimeFormatter DATE_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("HH:mm:ss:SSSS").withZone(ZoneOffset.UTC);

    public QueryMetricsTextWriter(StringBuilder stringBuilder) {
        assert stringBuilder != null;
        this.stringBuilder = stringBuilder;
    }

    @Override
    protected void writeBeforeQueryMetrics() {
        // Do Nothing
    }

    @Override
    protected void writeRetrievedDocumentCount(long retrievedDocumentCount) {
        QueryMetricsTextWriter.appendCountToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.RETRIEVED_DOCUMENT_COUNT, retrievedDocumentCount, 0);
    }

    @Override
    protected void writeRetrievedDocumentSize(long retrievedDocumentSize) {
        QueryMetricsTextWriter.appendBytesToStringBuilder(stringBuilder, QueryMetricsTextWriter.RETRIEVED_DOCUMENT_SIZE
                , retrievedDocumentSize, 0);
    }

    @Override
    protected void writeOutputDocumentCount(long outputDocumentCount) {
        QueryMetricsTextWriter.appendCountToStringBuilder(stringBuilder, QueryMetricsTextWriter.OUTPUT_DOCUMENT_COUNT,
                outputDocumentCount, 0);
    }

    @Override
    protected void writeOutputDocumentSize(long outputDocumentSize) {
        QueryMetricsTextWriter.appendBytesToStringBuilder(stringBuilder, QueryMetricsTextWriter.OUTPUT_DOCUMENT_SIZE,
                outputDocumentSize, 0);
    }

    @Override
    protected void writeIndexHitRatio(double indexHitRatio) {
        QueryMetricsTextWriter.appendPercentageToStringBuilder(stringBuilder, QueryMetricsTextWriter.INDEX_UTILIZATION_TEXT
                , indexHitRatio, 0);
    }

    @Override
    protected void writeTotalQueryExecutionTime(Duration totalQueryExecutionTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.TOTAL_QUERY_EXECUTION_TIME, durationToMilliseconds(totalQueryExecutionTime), 0);
    }

    @Override
    protected void writeBeforeQueryPreparationTimes() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.QUERY_PREPARATION_TIMES, 1);
    }

    @Override
    protected void writeQueryCompilationTime(Duration queryCompilationTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.QUERY_COMPILATION_TIME, durationToMilliseconds(queryCompilationTime), 2);
    }

    @Override
    protected void writeLogicalPlanBuildTime(Duration logicalPlanBuildTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.LOGICAL_PLAN_BUILD_TIME, durationToMilliseconds(logicalPlanBuildTime), 2);
    }

    @Override
    protected void writePhysicalPlanBuildTime(Duration physicalPlanBuildTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.PHYSICAL_PLAN_BUILD_TIME, durationToMilliseconds(physicalPlanBuildTime), 2);
    }

    @Override
    protected void writeQueryOptimizationTime(Duration queryOptimizationTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.QUERY_OPTIMIZATION_TIME, durationToMilliseconds(queryOptimizationTime), 2);
    }

    @Override
    protected void writeAfterQueryPreparationTimes() {
        // Do Nothing
    }

    @Override
    protected void writeIndexLookupTime(Duration indexLookupTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.INDEX_LOOKUP_TIME, durationToMilliseconds(indexLookupTime), 1);
    }

    @Override
    protected void writeDocumentLoadTime(Duration documentLoadTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.DOCUMENT_LOAD_TIME, durationToMilliseconds(documentLoadTime), 1);
    }

    @Override
    protected void writeVMExecutionTime(Duration vMExecutionTime) {
        // Do Nothing
    }

    @Override
    protected void writeBeforeRuntimeExecutionTimes() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.RUNTIME_EXECUTION_TIMES, 1);
    }

    @Override
    protected void writeQueryEngineExecutionTime(Duration queryEngineExecutionTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.QUERY_ENGINE_TIMES, durationToMilliseconds(queryEngineExecutionTime), 2);
    }

    @Override
    protected void writeSystemFunctionExecutionTime(Duration systemFunctionExecutionTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.SYSTEM_FUNCTION_EXECUTION_TIME, durationToMilliseconds(systemFunctionExecutionTime)
                , 2);
    }

    @Override
    protected void writeUserDefinedFunctionExecutionTime(Duration userDefinedFunctionExecutionTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.USER_DEFINED_FUNCTION_EXECUTION_TIME,
                durationToMilliseconds(userDefinedFunctionExecutionTime), 2);
    }

    @Override
    protected void writeAfterRuntimeExecutionTimes() {
        // Do Nothing
    }

    @Override
    protected void writeDocumentWriteTime(Duration documentWriteTime) {
        QueryMetricsTextWriter.appendMillisecondsToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.DOCUMENT_WRITE_TIME, durationToMilliseconds(documentWriteTime), 1);
    }

    @Override
    protected void writeBeforeClientSideMetrics() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder,
                QueryMetricsTextWriter.CLIENT_SIDE_METRICS, 0);
    }

    @Override
    protected void writeRetries(long retries) {
        QueryMetricsTextWriter.appendCountToStringBuilder(stringBuilder, QueryMetricsTextWriter.RETRIES, retries, 1);
    }

    @Override
    protected void writeRequestCharge(double requestCharge) {
        QueryMetricsTextWriter.appendRUToStringBuilder(stringBuilder, QueryMetricsTextWriter.REQUEST_CHARGE,
                requestCharge, 1);
    }

    @Override
    protected void writeBeforePartitionExecutionTimeline() {
        QueryMetricsTextWriter.appendNewlineToStringBuilder(stringBuilder);

        // Building the table for fetch execution ranges
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, QueryMetricsTextWriter.FETCH_EXECUTION_RANGES
                , 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, PARTITION_EXECUTION_TIMELINE_TABLE.getTopLine(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, PARTITION_EXECUTION_TIMELINE_TABLE.getHeader(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, PARTITION_EXECUTION_TIMELINE_TABLE.getMiddleLine(), 1);
    }

    @Override
    protected void writeBeforeFetchExecutionRange() {
        // Do Nothing
    }

    @Override
    protected void writeFetchPartitionKeyRangeId(String partitionId) {
        this.lastFetchPartitionId = partitionId;
    }

    @Override
    protected void writeActivityId(String activityId) {
        this.lastActivityId = activityId;
    }

    @Override
    protected void writeStartTime(Instant startTime) {
        this.lastStartTime = startTime;
    }

    @Override
    protected void writeEndTime(Instant endTime) {
        this.lastEndTime = endTime;
    }

    @Override
    protected void writeFetchDocumentCount(long numberOfDocuments) {
        this.lastFetchDocumentCount = numberOfDocuments;
    }

    @Override
    protected void writeFetchRetryCount(long retryCount) {
        this.lastFetchRetryCount = retryCount;
    }

    @Override
    protected void writeAfterFetchExecutionRange() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(
                stringBuilder,
                PARTITION_EXECUTION_TIMELINE_TABLE.getRow(Arrays.asList(
                        this.lastFetchPartitionId,
                        this.lastActivityId,
                        DATE_TIME_FORMATTER.format(this.lastStartTime),
                        DATE_TIME_FORMATTER.format(this.lastEndTime),
                        nanosToMilliSeconds(this.lastEndTime.minusNanos(lastStartTime.getNano()).getNano()),
                        this.lastFetchDocumentCount,
                        this.lastFetchRetryCount)),
                1);
    }

    @Override
    protected void writeAfterPartitionExecutionTimeline() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, PARTITION_EXECUTION_TIMELINE_TABLE.getBottomLine(),
                1);
    }

    @Override
    protected void writeBeforeSchedulingMetrics() {
        QueryMetricsTextWriter.appendNewlineToStringBuilder(stringBuilder);

        // Building the table for scheduling metrics
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, QueryMetricsTextWriter.SCHEDULING_METRICS, 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, SCHEDULING_METRICS_TABLE.getTopLine(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, SCHEDULING_METRICS_TABLE.getHeader(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, SCHEDULING_METRICS_TABLE.getMiddleLine(), 1);
    }

    @Override
    protected void writeBeforePartitionSchedulingDuration() {
        // Do Nothing
    }

    @Override
    protected void writePartitionSchedulingDurationId(String partitionId) {
        this.lastSchedulingPartitionId = partitionId;
    }

    @Override
    protected void writeResponseTime(long responseTime) {
        this.lastResponseTime = responseTime;
    }

    @Override
    protected void writeRunTime(long runTime) {
        this.lastRunTime = runTime;
    }

    @Override
    protected void writeWaitTime(long waitTime) {
        this.lastWaitTime = waitTime;
    }

    @Override
    protected void writeTurnaroundTime(long turnaroundTime) {
        this.lastTurnaroundTime = turnaroundTime;
    }

    @Override
    protected void writeNumberOfPreemptions(long numPreemptions) {
        this.lastNumberOfPreemptions = numPreemptions;
    }

    @Override
    protected void writeAfterPartitionSchedulingDuration() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(
                stringBuilder,
                SCHEDULING_METRICS_TABLE.getRow(Arrays.asList(
                        this.lastSchedulingPartitionId,
                        this.lastResponseTime,
                        this.lastRunTime,
                        this.lastWaitTime,
                        this.lastTurnaroundTime,
                        this.lastNumberOfPreemptions)),
                1);
    }

    @Override
    protected void writeAfterSchedulingMetrics() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, SCHEDULING_METRICS_TABLE.getBottomLine(), 1);
    }

    @Override
    protected void writeAfterClientSideMetrics() {
        // Do Nothing
    }

    // Index Utilisation Info Metrics
    @Override
    protected void writeBeforeIndexUtilizationInfoMetrics() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder,
            QueryMetricsTextWriter.INDEX_UTILIZATION_INFO_METRICS, 0);
    }

    @Override
    protected void writeBeforeUtilizedSingleIndexesMetrics() {
        QueryMetricsTextWriter.appendNewlineToStringBuilder(stringBuilder);

        // Building the table for Utilized Single Indexes Metrics
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, QueryMetricsTextWriter.UTILIZED_SINGLE_INDEXES_METRICS, 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getTopLine(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getHeader(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getMiddleLine(), 1);
    }

    @Override
    protected void writeBeforeUtilizedSingleIndex() {
        //Do Nothing
    }

    @Override
    protected void writeUtilizedSingleFilterExpression(String filterExpression) {
        this.lastUtilizedSingleFilterExpression = filterExpression;
    }

    @Override
    protected void writeUtilizedSingleIndexDocumentExpression(String indexDocumentExpression) {
        this.lastUtilizedSingleIndexDocumentExpression = indexDocumentExpression;
    }

    @Override
    protected void writeUtilizedSingleFilterExpressionPrecision(boolean filterExpressionPrecision) {
        this.lastUtilizedSingleFilterExpressionPrecision = filterExpressionPrecision;
    }

    @Override
    protected void writeUtilizedSingleIndexPlanFullFidelity(boolean indexPlanFullFidelity) {
        this.lastUtilizedSingleIndexPlanFullFidelity = indexPlanFullFidelity;
    }

    @Override
    protected void writeUtilizedSingleIndexImpactScore(String indexImpactScore) {
        this.lastUtilizedSingleIndexImpactScore = indexImpactScore;
    }

    @Override
    protected void writeAfterUtilizedSingleIndex() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(
            stringBuilder,
            UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getRow(Arrays.asList(
                this.lastUtilizedSingleFilterExpression,
                this.lastUtilizedSingleIndexDocumentExpression,
                this.lastUtilizedSingleFilterExpressionPrecision,
                this.lastUtilizedSingleIndexPlanFullFidelity,
                this.lastUtilizedSingleIndexImpactScore)),
            1);
    }

    @Override
    protected void writeAfterUtilizedSingleIndexesMetrics() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getBottomLine(),
            1);
    }

    @Override
    protected void writeBeforePotentialSingleIndexesMetrics() {
        QueryMetricsTextWriter.appendNewlineToStringBuilder(stringBuilder);

        // Building the table for Potential Single Indexes Metrics
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, QueryMetricsTextWriter.POTENTIAL_SINGLE_INDEXES_METRICS, 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getTopLine(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getHeader(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getMiddleLine(), 1);
    }

    @Override
    protected void writeBeforePotentialSingleIndex() {
        // Do Nothing
    }

    @Override
    protected void writePotentialSingleFilterExpression(String filterExpression) {
        this.lastPotentialSingleFilterExpression = filterExpression;
    }

    @Override
    protected void writePotentialSingleIndexDocumentExpression(String indexDocumentExpression) {
        this.lastPotentialSingleIndexDocumentExpression = indexDocumentExpression;
    }

    @Override
    protected void writePotentialSingleFilterExpressionPrecision(boolean filterExpressionPrecision) {
        this.lastPotentialSingleFilterExpressionPrecision = filterExpressionPrecision;
    }

    @Override
    protected void writePotentialSingleIndexPlanFullFidelity(boolean indexPlanFullFidelity) {
        this.lastPotentialSingleIndexPlanFullFidelity = indexPlanFullFidelity;
    }

    @Override
    protected void writePotentialSingleIndexImpactScore(String indexImpactScore) {
        this.lastPotentialSingleIndexImpactScore = indexImpactScore;
    }

    @Override
    protected void writeAfterPotentialSingleIndex() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(
            stringBuilder,
            UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getRow(Arrays.asList(
                this.lastPotentialSingleFilterExpression,
                this.lastPotentialSingleIndexDocumentExpression,
                this.lastPotentialSingleFilterExpressionPrecision,
                this.lastPotentialSingleIndexPlanFullFidelity,
                this.lastPotentialSingleIndexImpactScore)),
            1);
    }

    @Override
    protected void writeAfterPotentialSingleIndexesMetrics() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_SINGLE_INDEXES_TABLE.getBottomLine(),
            1);
    }

    @Override
    protected void writeBeforeUtilizedCompositeIndexesMetrics() {
        QueryMetricsTextWriter.appendNewlineToStringBuilder(stringBuilder);

        // Building the table for Utilized Composite Indexes Metrics
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, QueryMetricsTextWriter.UTILIZED_COMPOSITE_INDEXES_METRICS, 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getTopLine(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getHeader(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getMiddleLine(), 1);
    }

    @Override
    protected void writeBeforeUtilizedCompositeIndex() {
        // Do Nothing
    }

    @Override
    protected void writeUtilizedCompositeIndexDocumentExpressions(List<String> indexDocumentExpressions) {
        this.lastUtilizedCompositeIndexDocumentExpressions = indexDocumentExpressions;
    }

    @Override
    protected void writeUtilizedCompositeIndexPlanFullFidelity(boolean indexPlanFullFidelity) {
        this.lastUtilizedCompositeIndexPlanFullFidelity = indexPlanFullFidelity;
    }

    @Override
    protected void writeUtilizedCompositeIndexImpactScore(String indexImpactScore) {
        this.lastUtilizedCompositeIndexImpactScore = indexImpactScore;
    }

    @Override
    protected void writeAfterUtilizedCompositeIndex() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(
            stringBuilder,
            UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getRow(Arrays.asList(
                this.lastUtilizedCompositeIndexDocumentExpressions,
                this.lastUtilizedCompositeIndexPlanFullFidelity,
                this.lastUtilizedCompositeIndexImpactScore
            )),
            1);
    }

    @Override
    protected void writeAfterUtilizedCompositeIndexesMetrics() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getBottomLine(),
            1);
    }

    @Override
    protected void writeBeforePotentialCompositeIndexesMetrics() {
        QueryMetricsTextWriter.appendNewlineToStringBuilder(stringBuilder);

        // Building the table for Potential Composite Indexes Metrics
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, QueryMetricsTextWriter.POTENTIAL_COMPOSITE_INDEXES_METRICS, 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getTopLine(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getHeader(), 1);
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getMiddleLine(), 1);
    }

    @Override
    protected void writeBeforePotentialCompositeIndex() {
        // Do Nothing
    }

    @Override
    protected void writePotentialCompositeIndexDocumentExpressions(List<String> indexDocumentExpressions) {
        this.lastPotentialCompositeIndexDocumentExpressions = indexDocumentExpressions;
    }

    @Override
    protected void writePotentialCompositeIndexPlanFullFidelity(boolean indexPlanFullFidelity) {
        this.lastPotentialCompositeIndexPlanFullFidelity = indexPlanFullFidelity;
    }

    @Override
    protected void writePotentialCompositeIndexImpactScore(String indexImpactScore) {
        this.lastPotentialCompositeIndexImpactScore = indexImpactScore;
    }

    @Override
    protected void writeAfterPotentialCompositeIndex() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(
            stringBuilder,
            UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getRow(Arrays.asList(
                this.lastPotentialCompositeIndexDocumentExpressions,
                this.lastPotentialCompositeIndexPlanFullFidelity,
                this.lastPotentialCompositeIndexImpactScore
            )),
            1);
    }

    @Override
    protected void writeAfterPotentialCompositeIndexesMetrics() {
        QueryMetricsTextWriter.appendHeaderToStringBuilder(stringBuilder, UTILIZED_OR_POTENTIAL_COMPOSITE_INDEXES_TABLE.getBottomLine(),
            1);
    }

    @Override
    protected void writeAfterIndexUtilizationInfoMetrics() {
        // Do Nothing
    }

    @Override
    protected void writeAfterQueryMetrics() {
        // Do Nothing
    }

    // Util functions
    private static final int NANOS_TO_MILLIS = 1000000;

    static HashMap<String, Double> parseDelimitedString(String delimitedString) {
        if (delimitedString == null) {
            throw new NullPointerException("delimitedString");
        }

        HashMap<String, Double> metrics = new HashMap<>();

        final int key = 0;
        final int value = 1;
        String[] headerAttributes = StringUtils.split(delimitedString, ";");

        for (String attribute : headerAttributes) {
            String[] attributeKeyValue = StringUtils.split(attribute, "=");

            if (attributeKeyValue.length != 2) {
                throw new NullPointerException("recieved a malformed delimited STRING");
            }

            String attributeKey = attributeKeyValue[key];
            double attributeValue = Double.parseDouble(attributeKeyValue[value]);
            metrics.put(attributeKey, attributeValue);
        }

        return metrics;
    }

    static Duration durationFromMetrics(HashMap<String, Double> metrics, String key) {
        // Just attempt to get the metrics
        Double durationInMilliseconds = metrics.get(key);
        if (durationInMilliseconds == null) {
            return Duration.ZERO;
        }

        long seconds = (long) (durationInMilliseconds / 1e3);
        long nanoseconds = (long) ((durationInMilliseconds - (seconds * 1e3)) * 1e6);

        return Duration.ofSeconds(seconds, nanoseconds);
    }

    static double durationToMilliseconds(Duration duration) {
        double seconds = duration.getSeconds();
        double nano = duration.getNano();

        return (seconds * 1e3) + (nano / 1e6);
    }

    static Duration getDurationFromMetrics(HashMap<String, Double> metrics, String key) {
        double timeSpanInMilliseconds;
        Duration timeSpanFromMetrics;
        timeSpanInMilliseconds = metrics.get(key);
        timeSpanFromMetrics = doubleMillisecondsToDuration(timeSpanInMilliseconds);
        return timeSpanFromMetrics;
    }

    private static Duration doubleMillisecondsToDuration(double timeSpanInMilliseconds) {
        long timeInNanoSeconds = (long) (timeSpanInMilliseconds * NANOS_TO_MILLIS);
        return Duration.ofNanos(timeInNanoSeconds);
    }

    private static void appendToStringBuilder(StringBuilder stringBuilder, String property, String value,
                                              String units, int indentLevel) {
        final String Indent = "  ";
        final String FormatString = "%-40s : %15s %-12s %s";

        stringBuilder.append(String.format(
                Locale.ROOT,
                FormatString,
                StringUtils.repeat(Indent, indentLevel) + property,
                value,
                units,
                System.lineSeparator()));
    }

    static void appendBytesToStringBuilder(StringBuilder stringBuilder, String property, long bytes, int indentLevel) {
        final String BytesFormatString = "%d";
        final String BytesUnitString = "bytes";

        appendToStringBuilder(
                stringBuilder,
                property,
                String.format(BytesFormatString, bytes),
                BytesUnitString,
                indentLevel);
    }

    static void appendMillisecondsToStringBuilder(StringBuilder stringBuilder, String property, double milliseconds,
                                                  int indentLevel) {
        final String MillisecondsFormatString = "%f";
        final String MillisecondsUnitString = "milliseconds";

        appendToStringBuilder(stringBuilder, property, String.format(MillisecondsFormatString,
                milliseconds), MillisecondsUnitString, indentLevel);
    }

    static void appendNanosecondsToStringBuilder(StringBuilder stringBuilder, String property, double nanoSeconds,
                                                 int indentLevel) {
        final String MillisecondsFormatString = "%.2f";
        final String MillisecondsUnitString = "milliseconds";
        appendToStringBuilder(stringBuilder, property, String.format(MillisecondsFormatString,
                nanosToMilliSeconds(nanoSeconds)), MillisecondsUnitString, indentLevel);
    }

    static double nanosToMilliSeconds(double nanos) {
        return nanos / NANOS_TO_MILLIS;
    }

    static void appendHeaderToStringBuilder(StringBuilder stringBuilder, String headerTitle, int indentLevel) {
        final String Indent = "  ";
        final String FormatString = "%s %s";
        stringBuilder.append(String.format(
                Locale.ROOT,
                FormatString,
                String.join(StringUtils.repeat(Indent, indentLevel)) + headerTitle,
                System.lineSeparator()));
    }

    static void appendRUToStringBuilder(StringBuilder stringBuilder, String property, double requestCharge,
                                        int indentLevel) {
        final String RequestChargeFormatString = "%s";
        final String RequestChargeUnitString = "RUs";

        appendToStringBuilder(
                stringBuilder,
                property,
                String.format(Locale.ROOT, RequestChargeFormatString, requestCharge),
                RequestChargeUnitString,
                indentLevel);
    }

    static void appendActivityIdsToStringBuilder(StringBuilder stringBuilder, String activityIdsLabel,
                                                 List<String> activityIds, int indentLevel) {
        final String Indent = "  ";
        stringBuilder.append(activityIdsLabel);
        stringBuilder.append(System.lineSeparator());
        for (String activityId : activityIds) {
            stringBuilder.append(Indent);
            stringBuilder.append(activityId);
            stringBuilder.append(System.lineSeparator());
        }
    }

    static void appendPercentageToStringBuilder(StringBuilder stringBuilder, String property, double percentage,
                                                int indentLevel) {
        final String PercentageFormatString = "%.2f";
        final String PercentageUnitString = "%";

        appendToStringBuilder(stringBuilder, property, String.format(PercentageFormatString,
                percentage * 100), PercentageUnitString, indentLevel);
    }

    static void appendCountToStringBuilder(StringBuilder stringBuilder, String property, long count, int indentLevel) {
        final String CountFormatString = "%s";
        final String CountUnitString = "";

        appendToStringBuilder(
                stringBuilder,
                property,
                String.format(CountFormatString, count),
                CountUnitString,
                indentLevel);
    }

    static void appendNewlineToStringBuilder(StringBuilder stringBuilder) {
        appendHeaderToStringBuilder(stringBuilder, StringUtils.EMPTY, 0);
    }

}