"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TelemetryKustoClient = void 0;
const Logger_1 = require("./Logger");
const internal_1 = require("../../internal");
const Network_1 = require("../Network");
class TelemetryKustoClient {
    /**
     * Constructs a telemetry client for ingesting logs into Kusto.
     * @param ctx The CDM corpus context.
     * @param config The configuration for the client.
     */
    constructor(ctx, config) {
        /**
         * Maximum number of retries allowed for an HTTP request.
         */
        this.maxNumRetries = 5;
        /**
         * Maximum timeout for completing an HTTP request including retries.
         */
        this.maxTimeoutMilliseconds = 10000;
        /**
         * Maximum timeout for a single HTTP request.
         */
        this.timeoutMilliseconds = 1000;
        /**
         * Indicates whether the telemetry client is enabled or not.
         */
        this.isEnabled = false;
        /**
         * Kusto data ingestion command.
         */
        this.ingestionCommand = '.ingest inline into table';
        /**
         * The frequency in millisecond by which to check and ingest the request queue.
         */
        this.ingestionFrequency = 500;
        /**
         * The maximum size of the request queue.
         */
        this.requestQueueMaxSize = 200;
        this.logExecTimeMethods = [
            'createResolvedEntityAsync',
            'calculateEntityGraphAsync',
            'calculateEntityGraphAsync(perEntity)'
        ];
        this.ctx = ctx;
        this.config = config;
        this.httpClient = new Network_1.CdmHttpClient();
        this.requestQueue = new Array();
    }
    /**
     * Enqueue the request queue with the information to be logged.
     * @param timestamp The log timestamp.
     * @param level Logging status level.
     * @param className Usually the class that is calling the method.
     * @param method Usually denotes method calling this method.
     * @param corpusPath Usually denotes corpus path of document.
     * @param message Informational message.
     * @param requireIngestion (Optional) Whether the log needs to be ingested.
     * @param code (Optional) Error or warning code.
     */
    addToIngestionQueue(timestamp, level, className, method, corpusPath, message, requireIngestion = false, code = internal_1.cdmLogCode.None) {
        // Check if the Kusto config and the concurrent queue has been initialized
        if (this.config === undefined || this.requestQueue === undefined) {
            return;
        }
        // Skip if the telemetry client is not enabled
        if (!this.isEnabled) {
            return;
        }
        // Not ingest logs from telemetry client to avoid cycling
        if (className === TelemetryKustoClient.name) {
            return;
        }
        // If ingestion is not required and the level is Progress
        if (level === internal_1.cdmStatusLevel.progress && !requireIngestion) {
            // If the execution time needs to be logged
            if (this.logExecTimeMethods.includes(method)) {
                // Check if the log contains execution time info
                const execTimeMessage = 'Leaving scope. Time elapsed:';
                // Skip if the log is not for execution time
                if (!message.startsWith(execTimeMessage)) {
                    return;
                }
            }
            // Skip if the method execution time doesn't need to be logged
            else {
                return;
            }
        }
        // Configured in case no user-created content can be ingested into Kusto due to compliance issue
        // Note: The RemoveUserContent property could be deleted in the if the compliance issue gets resolved
        if (this.config.removeUserContent) {
            corpusPath = null;
            if (level === internal_1.cdmStatusLevel.warning || level === internal_1.cdmStatusLevel.error) {
                message = null;
            }
        }
        const logEntry = this.processLogEntry(timestamp, className, method, message, code, corpusPath, this.ctx.correlationId, this.ctx.events.apiCorrelationId, this.ctx.corpus.appId);
        // Add the status level and log entry to the queue to be ingested
        this.requestQueue.push([level, logEntry]);
        const currentTime = new Date();
        // Ingest when request queue exceeds max size or by ingestion frequency
        if (this.requestQueue.length >= this.requestQueueMaxSize ||
            (currentTime.valueOf() - this.lastIngestionTime.valueOf()) > this.ingestionFrequency) {
            this.lastIngestionTime = currentTime;
            this.ingestRequestQueue();
        }
    }
    /**
     * Check if all the requests have been ingested.
     * @returns A Boolean indicating whether the queue is empty or not.
     */
    checkRequestQueueIsEmpty() {
        if (this.requestQueue === undefined || this.requestQueue.length === 0) {
            return true;
        }
        return false;
    }
    /**
     * Enable the telemetry client which ingests logs into Kusto database.
     */
    enable() {
        // Check if the Kusto config and the concurrent queue has been initialized
        if (this.config == null || this.requestQueue == null) {
            const message = "The telemetry client has not been initialized";
            Logger_1.Logger.warning(this.ctx, TelemetryKustoClient.name, "enable", null, internal_1.cdmLogCode.WarnTelemetryIngestionFailed, message);
            return;
        }
        this.isEnabled = true;
        this.lastIngestionTime = new Date();
    }
    /**
     * Get an authorization token and send the query to Kusto.
     * @param query The Kusto query command to be posted to the cluster.
     */
    postKustoQuery(query) {
        return __awaiter(this, void 0, void 0, function* () {
            const authToken = yield this.config.getAuthenticationToken();
            const queryEndpoint = `https://${this.config.kustoClusterName}.kusto.windows.net/v1/rest/mgmt`;
            const kustoHost = `${this.config.kustoClusterName}.kusto.windows.net`;
            const queryBody = `{"db":"${this.config.kustoDatabaseName}","csl":"${query}"}`;
            const headers = new Map([
                ['Accept', 'application/json'],
                ['Authorization', authToken],
                ['Host', kustoHost]
            ]);
            const httpRequest = new Network_1.CdmHttpRequest(queryEndpoint);
            httpRequest.method = 'POST';
            httpRequest.headers = headers;
            httpRequest.content = queryBody;
            httpRequest.contentType = 'application/json';
            httpRequest.numberOfRetries = this.maxNumRetries;
            httpRequest.timeout = this.timeoutMilliseconds;
            httpRequest.maximumTimeout = this.maxTimeoutMilliseconds;
            const response = yield this.httpClient.SendAsync(httpRequest, this.getRetryWaitTime, this.ctx);
            if (response === undefined) {
                throw new Error('Kusto query post failed. The result of a request is undefined.');
            }
            if (!response.isSuccessful) {
                throw new Error(`Kusto query post failed. HTTP ${response.statusCode} - ${response.reason}.`);
            }
        });
    }
    /**
     * A callback function for http request retries.
     * @param response The Http response.
     * @param hasFailed Indicates whether the request has failed.
     * @param retryNumber The number of retries happened.
     * @returns The wait time before starting the next retry.
     */
    getRetryWaitTime(response, hasFailed, retryNumber) {
        if (response !== undefined && ((response.isSuccessful && !hasFailed))) {
            return undefined;
        }
        else {
            // Default wait time is calculated using exponential backoff with with random jitter value to avoid 'waves'.
            const upperBound = 1 << retryNumber;
            return Math.floor(Math.random() * upperBound) * TelemetryKustoClient.backoffForThrottling;
        }
    }
    /**
     * Ingest log entries into the table specified.
     * @param logTable The table to be ingested into.
     * @param logEntries Batched log entries.
     */
    ingestIntoTable(logTable, logEntries) {
        return __awaiter(this, void 0, void 0, function* () {
            if (logEntries !== undefined && logEntries.length !== 0) {
                const query = `${this.ingestionCommand} ${logTable} <|\n${logEntries}`;
                try {
                    yield this.postKustoQuery(query);
                }
                catch (e) {
                    Logger_1.Logger.warning(this.ctx, TelemetryKustoClient.name, this.ingestIntoTable.name, null, internal_1.cdmLogCode.WarnTelemetryIngestionFailed, e);
                }
            }
        });
    }
    /**
     * Ingest all the logs existing in the request queue.
     */
    ingestRequestQueue() {
        return __awaiter(this, void 0, void 0, function* () {
            // Dequeue and send the ingestion request if the queue is not empty
            if (this.requestQueue.length !== 0) {
                // Batch log entries for each table
                let infoLogEntries = '';
                let warningLogEntries = '';
                let errorLogEntries = '';
                let request;
                // Try to dequeue the first request in the queue
                while ((request = this.requestQueue.shift()) !== undefined) {
                    const logEntry = request[1];
                    switch (request[0]) {
                        case internal_1.cdmStatusLevel.progress:
                            infoLogEntries += logEntry;
                            break;
                        case internal_1.cdmStatusLevel.warning:
                            warningLogEntries += logEntry;
                            break;
                        case internal_1.cdmStatusLevel.error:
                            errorLogEntries += logEntry;
                            break;
                        default:
                            break;
                    }
                }
                // Ingest logs into corresponding tables in the Kusto database
                if (this.ctx !== undefined && this.config !== undefined) {
                    if (infoLogEntries.length !== 0) {
                        yield this.ingestIntoTable(this.config.kustoInfoLogTable, infoLogEntries);
                    }
                    if (warningLogEntries.length !== 0) {
                        yield this.ingestIntoTable(this.config.kustoWarningLogTable, warningLogEntries);
                    }
                    if (errorLogEntries.length !== 0) {
                        yield this.ingestIntoTable(this.config.kustoErrorLogTable, errorLogEntries);
                    }
                }
            }
        });
    }
    /**
     * Process the input log information to remove all unauthorized information.
     * @param timestamp The log timestamp.
     * @param className Usually the class that is calling the method.
     * @param method Usually denotes method calling this method.
     * @param message Informational message.
     * @param logCode Error code, usually empty.
     * @param corpusPath Usually denotes corpus path of document.
     * @param correlationId The corpus correlation id.
     * @param apiCorrelationId The method correlation id.
     * @param appId The app id assigned by user.
     * @return A complete log entry.
     */
    processLogEntry(timestamp, className, method, message, logCode, corpusPath, correlationId, apiCorrelationId, appId) {
        // Remove user created contents
        if (this.config.ingestAtLevel === internal_1.environmentType.PROD || this.config.ingestAtLevel === internal_1.environmentType.TEST) {
            corpusPath = null;
        }
        if (message === null || message === undefined) {
            message = '';
        }
        // Remove all commas from message to ensure the correct syntax of Kusto query
        message = message.replace(',', ';');
        const code = internal_1.cdmLogCode[logCode];
        // Additional properties associated with the log
        const property = new Map([
            ['Environment', this.config.ingestAtLevel.toString()],
            ['SDKLanguage', 'TypeScript'],
            ['Region', this.config.region]
        ]);
        const propertyJson = this.serializeMap(property);
        return `${timestamp},${className},${method},${message},${code},${corpusPath},${correlationId},${apiCorrelationId},${appId},${propertyJson}\n`;
    }
    /**
     * Serialize the map and return a string.
     * @param map The map object to be serialized.
     * @returns The serialized map.
     */
    serializeMap(map) {
        let mapAsString = '';
        for (const key of map.keys()) {
            mapAsString += `${key}:${map.get(key)};`;
        }
        return mapAsString;
    }
}
exports.TelemetryKustoClient = TelemetryKustoClient;
/**
 * The time in millisecond to pause sending http request when throttling.
 */
TelemetryKustoClient.backoffForThrottling = 200;
//# sourceMappingURL=TelemetryKustoClient.js.map