"use strict";
/*!
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Log = void 0;
const promisify_1 = require("@google-cloud/promisify");
const dotProp = require("dot-prop");
const extend = require("extend");
const entry_1 = require("./entry");
const instrumentation_1 = require("./utils/instrumentation");
const log_common_1 = require("./utils/log-common");
/**
 * A log is a named collection of entries, each entry representing a timestamped
 * event. Logs can be produced by Google Cloud Platform services, by third-party
 * services, or by your applications. For example, the log `apache-access` is
 * produced by the Apache Web Server, but the log
 * `compute.googleapis.com/activity_log` is produced by Google Compute Engine.
 *
 * See {@link https://cloud.google.com/logging/docs/basic-concepts#logs|Introduction to Logs}
 *
 * @class
 *
 * @param {Logging} logging {@link Logging} instance.
 * @param {string} name Name of the log.
 * @param {object} [options] Configuration object.
 * @param {boolean} [options.removeCircular] Replace circular references in
 *     logged objects with a string value, `[Circular]`. (Default: false)
 * @param {number} [options.maxEntrySize] A max entry size
 * @param {string[]} [options.jsonFieldsToTruncate] A list of JSON properties at the given full path to be truncated.
 *     Received values will be prepended to predefined list in the order received and duplicates discarded.
 * @param {ApiResponseCallback} [options.defaultWriteDeleteCallback] A default global callback to be used for {@link Log#write}
 *     and {@link Log#delete} APIs when {@link ApiResponseCallback} callback was not supplied by caller in function parameters.
 *     Note that {@link LogOptions#defaultWriteDeleteCallback} is useful when {@link Log#write} and {@link Log#delete} APIs are called
 *     without `await` and without callback added explicitly to every call - this way {@link LogOptions#defaultWriteDeleteCallback}
 *     can serve as global callback handler, which for example could be used to catch all errors and eliminate crashes.
 * @param {boolean} [options.partialSuccess] Global flag indicating Whether a batch's valid entries should be written even if
 *     some other entry failed due to errors. Default is true.
 *     See {@link https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write#body.request_body.FIELDS.partial_success|partialSuccess} for more info.
 * @example
 * ```
 * import {Logging} from '@google-cloud/logging';
 * import {LogOptions} from '@google-cloud/logging/build/src/log';
 * const options: LogOptions = {
 *   maxEntrySize: 256,
 *   jsonFieldsToTruncate: [
 *     'jsonPayload.fields.metadata.structValue.fields.custom.stringValue',
 *   ],
 *   defaultWriteDeleteCallback: (err: any) => {
 *     if (err) {
 *       console.log('Error: ' + err);
 *     }
 *   },
 * };
 * const logging = new Logging();
 * const log = logging.log('syslog', options);
 * ```
 */
class Log {
    constructor(logging, name, options) {
        var _a;
        options = options || {};
        this.formattedName_ = (0, log_common_1.formatLogName)(logging.projectId, name);
        this.removeCircular_ = options.removeCircular === true;
        this.maxEntrySize = options.maxEntrySize;
        this.logging = logging;
        /**
         * @name Log#name
         * @type {string}
         */
        this.name = this.formattedName_.split('/').pop();
        this.jsonFieldsToTruncate = [
            // Winston:
            'jsonPayload.fields.metadata.structValue.fields.stack.stringValue',
            // Bunyan:
            'jsonPayload.fields.msg.stringValue',
            'jsonPayload.fields.err.structValue.fields.stack.stringValue',
            'jsonPayload.fields.err.structValue.fields.message.stringValue',
            // All:
            'jsonPayload.fields.message.stringValue',
        ];
        // Prepend all custom fields to be truncated to a list with defaults, thus
        // custom fields will be truncated first. Make sure to filter out fields
        // which are not in EntryJson.jsonPayload
        if (options.jsonFieldsToTruncate !== null &&
            options.jsonFieldsToTruncate !== undefined) {
            const filteredList = options.jsonFieldsToTruncate.filter(str => str !== null &&
                !this.jsonFieldsToTruncate.includes(str) &&
                str.startsWith('jsonPayload'));
            const uniqueSet = new Set(filteredList);
            this.jsonFieldsToTruncate = Array.from(uniqueSet).concat(this.jsonFieldsToTruncate);
        }
        /**
         * The default callback for {@link Log#write} and {@link Log#delete} APIs
         * is going to be used only when {@link LogOptions#defaultWriteDeleteCallback}
         * was set by user and only for APIs which does not accept a callback as parameter
         */
        this.defaultWriteDeleteCallback = options.defaultWriteDeleteCallback;
        /**
        Turning partialSuccess by default to be true if not provided in options. This should improve
        overall logging reliability since only oversized entries will be dropped
        from request. See {@link https://cloud.google.com/logging/quotas#log-limits} for more info
         */
        this.partialSuccess = (_a = options.partialSuccess) !== null && _a !== void 0 ? _a : true;
    }
    alert(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'ALERT'), options);
    }
    critical(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'CRITICAL'), options);
    }
    debug(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'DEBUG'), options);
    }
    async delete(gaxOptions) {
        const projectId = await this.logging.auth.getProjectId();
        this.formattedName_ = (0, log_common_1.formatLogName)(projectId, this.name);
        const reqOpts = {
            logName: this.formattedName_,
        };
        return this.logging.loggingService.deleteLog(reqOpts, gaxOptions, this.defaultWriteDeleteCallback);
    }
    emergency(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'EMERGENCY'), options);
    }
    entry(metadataOrData, data) {
        let metadata;
        if (!data &&
            metadataOrData !== null &&
            Object.prototype.hasOwnProperty.call(metadataOrData, 'httpRequest')) {
            // If user logs entry(metadata.httpRequest)
            metadata = metadataOrData;
            data = {};
        }
        else if (!data) {
            // If user logs entry(message)
            data = metadataOrData;
            metadata = {};
        }
        else {
            // If user logs entry(metadata, message)
            metadata = metadataOrData;
        }
        return this.logging.entry(metadata, data);
    }
    error(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'ERROR'), options);
    }
    async getEntries(opts) {
        const options = extend({}, opts);
        const projectId = await this.logging.auth.getProjectId();
        this.formattedName_ = (0, log_common_1.formatLogName)(projectId, this.name);
        if (options.filter && !options.filter.includes('logName=')) {
            options.filter = `(${options.filter}) AND logName="${this.formattedName_}"`;
        }
        else if (!options.filter) {
            options.filter = `logName="${this.formattedName_}"`;
        }
        return this.logging.getEntries(options);
    }
    /**
     * This method is a wrapper around {module:logging#getEntriesStream}, but with
     * a filter specified to only return {module:logging/entry} objects from this
     * log.
     *
     * @method Log#getEntriesStream
     * @param {GetEntriesRequest} [query] Query object for listing entries.
     * @returns {ReadableStream} A readable stream that emits {@link Entry}
     *     instances.
     *
     * @example
     * ```
     * const {Logging} = require('@google-cloud/logging');
     * const logging = new Logging();
     * const log = logging.log('my-log');
     *
     * log.getEntriesStream()
     *   .on('error', console.error)
     *   .on('data', entry => {
     *     // `entry` is a Cloud Logging entry object.
     *     // See the `data` property to read the data from the entry.
     *   })
     *   .on('end', function() {
     *     // All entries retrieved.
     *   });
     *
     * //-
     * // If you anticipate many results, you can end a stream early to prevent
     * // unnecessary processing and API requests.
     * //-
     * log.getEntriesStream()
     *   .on('data', function(entry) {
     *     this.end();
     *   });
     * ```
     */
    getEntriesStream(options) {
        options = extend({
            log: this.name,
        }, options);
        return this.logging.getEntriesStream(options);
    }
    /**
     * This method is a wrapper around {module:logging#tailEntries}, but with
     * a filter specified to only return {module:logging/entry} objects from this
     * log.
     *
     * @method Log#tailEntries
     * @param {TailEntriesRequest} [query] Query object for tailing entries.
     * @returns {DuplexStream} A duplex stream that emits TailEntriesResponses
     * containing an array of {@link Entry} instances.
     *
     * @example
     * ```
     * const {Logging} = require('@google-cloud/logging');
     * const logging = new Logging();
     * const log = logging.log('my-log');
     *
     * log.tailEntries()
     *   .on('error', console.error)
     *   .on('data', resp => {
     *     console.log(resp.entries);
     *     console.log(resp.suppressionInfo);
     *   })
     *   .on('end', function() {
     *     // All entries retrieved.
     *   });
     *
     * //-
     * // If you anticipate many results, you can end a stream early to prevent
     * // unnecessary processing and API requests.
     * //-
     * log.tailEntries()
     *   .on('data', function(entry) {
     *     this.end();
     *   });
     * ```
     */
    tailEntries(options) {
        options = extend({
            log: this.name,
        }, options);
        return this.logging.tailEntries(options);
    }
    info(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'INFO'), options);
    }
    notice(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'NOTICE'), options);
    }
    warning(entry, options) {
        return this.write((0, log_common_1.assignSeverityToEntries)(entry, 'WARNING'), options);
    }
    async write(entry, opts) {
        var _a, _b, _c;
        const options = opts ? opts : {};
        // Extract projectId & resource from Logging - inject & memoize if not.
        await this.logging.setProjectId();
        this.formattedName_ = (0, log_common_1.formatLogName)(this.logging.projectId, this.name);
        const resource = await this.getOrSetResource(options);
        // Extract & format additional context from individual entries. Make sure to add instrumentation info
        const info = (0, instrumentation_1.populateInstrumentationInfo)(entry);
        const decoratedEntries = this.decorateEntries(info[0]);
        // If instrumentation info was added or this.partialSuccess was set, make sure we set
        // partialSuccess in outgoing write request, so entire request will make it through and
        // only oversized entries will be dropped if any
        if (info[1] || ((_a = options.partialSuccess) !== null && _a !== void 0 ? _a : this.partialSuccess)) {
            options.partialSuccess = true;
        }
        this.truncateEntries(decoratedEntries);
        // Clobber `labels` and `resource` fields with WriteOptions from the user.
        const reqOpts = extend({
            logName: this.formattedName_,
            entries: decoratedEntries,
            resource,
        }, options);
        delete reqOpts.gaxOptions;
        // Propagate maxRetries properly into writeLogEntries call
        if (!((_b = options.gaxOptions) === null || _b === void 0 ? void 0 : _b.maxRetries) && ((_c = this.logging.options) === null || _c === void 0 ? void 0 : _c.maxRetries)) {
            options.gaxOptions = extend({
                maxRetries: this.logging.options.maxRetries,
            }, options.gaxOptions);
        }
        return this.logging.loggingService.writeLogEntries(reqOpts, options.gaxOptions, this.defaultWriteDeleteCallback);
    }
    /**
     * getOrSetResource looks for GCP service context first at the user
     * declaration level (snakecasing keys), then in the Logging instance,
     * before finally detecting a resource from the environment.
     * The resource is then memoized at the Logging instance level for future use.
     *
     * @param options
     * @private
     */
    async getOrSetResource(options) {
        if (options.resource) {
            if (options.resource.labels)
                (0, log_common_1.snakecaseKeys)(options.resource.labels);
            return options.resource;
        }
        await this.logging.setDetectedResource();
        return this.logging.detectedResource;
    }
    /**
     * All entries are passed through here in order be formatted and serialized.
     * User provided Entry values are formatted per LogEntry specifications.
     * Read more about the LogEntry format:
     * https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
     *
     * @private
     *
     * @param {object[]} entries - Entry objects.
     * @returns {object[]} Serialized entries.
     * @throws if there is an error during serialization.
     */
    decorateEntries(entries) {
        return entries.map(entry => {
            if (!(entry instanceof entry_1.Entry)) {
                entry = this.entry(entry);
            }
            return entry.toJSON({
                removeCircular: this.removeCircular_,
            }, this.logging.projectId);
        });
    }
    // TODO consider refactoring `truncateEntries` so that it does not mutate
    /**
     * Truncate log entries at maxEntrySize, so that error is not thrown, see:
     * https://cloud.google.com/logging/quotas
     *
     * @private
     *
     * @param {object|string} the JSON log entry.
     * @returns {object|string} truncated JSON log entry.
     */
    truncateEntries(entries) {
        return entries.forEach(entry => {
            if (this.maxEntrySize === undefined)
                return;
            const payloadSize = JSON.stringify(entry).length;
            if (payloadSize < this.maxEntrySize)
                return;
            let delta = payloadSize - this.maxEntrySize;
            if (entry.textPayload) {
                entry.textPayload = entry.textPayload.slice(0, Math.max(entry.textPayload.length - delta, 0));
            }
            else {
                for (const field of this.jsonFieldsToTruncate) {
                    const msg = dotProp.get(entry, field, '');
                    if (msg !== null && msg !== undefined && msg !== '') {
                        dotProp.set(entry, field, msg.slice(0, Math.max(msg.length - delta, 0)));
                        delta -= Math.min(msg.length, delta);
                        if (delta <= 0) {
                            break;
                        }
                    }
                }
            }
        });
    }
    // TODO: in a future breaking release, delete this extranenous function.
    /**
     * Return an array of log entries with the desired severity assigned.
     *
     * @private
     *
     * @param {object|object[]} entries - Log entries.
     * @param {string} severity - The desired severity level.
     */
    static assignSeverityToEntries_(entries, severity) {
        return (0, log_common_1.assignSeverityToEntries)(entries, severity);
    }
    // TODO: in a future breaking release, delete this extranenous function.
    /**
     * Format the name of a log. A log's full name is in the format of
     * 'projects/{projectId}/logs/{logName}'.
     *
     * @private
     *
     * @returns {string}
     */
    static formatName_(projectId, name) {
        return (0, log_common_1.formatLogName)(projectId, name);
    }
}
exports.Log = Log;
/*! Developer Documentation
 *
 * All async methods (except for streams) will call a callback in the event
 * that a callback is provided .
 */
(0, promisify_1.callbackifyAll)(Log, { exclude: ['entry', 'getEntriesStream'] });
//# sourceMappingURL=log.js.map