cf-node-client
momo auto_awesome BUY CLAUDE KIT WITH 20% OFF coffee BUY ME COFFEE

Source: model/metrics/Logs.js

"use strict";

const CloudControllerBase = require("../cloudcontroller/CloudControllerBase");

/**
 * Logs — CF log retrieval and parsing.
 * Extends CloudControllerBase for shared HttpUtils / HttpStatus / token management.
 * {@link https://docs.pivotal.io/pivotalcf/devguide/deploy-apps/streaming-logs.html}
 *
 * @class
 */
class Logs extends CloudControllerBase {

    /**
     * @param {String} [endPoint] - Logging endpoint URL
     */
    constructor(endPoint) {
        super(endPoint);
    }

    /**
     * Get raw recent logs for an application.
     * @param {String} appGuid - Application GUID
     * @return {Promise<String>} Raw log string
     */
    getRecent(appGuid) {
        const options = {
            method: "GET",
            url: `${this.API_URL}/recent?app=${appGuid}`,
            headers: { Authorization: this.getAuthorizationHeader() }
        };
        return this.REST.request(options, this.HttpStatus.OK, false);
    }

    /**
     * Get recent logs parsed into structured objects.
     * @param {String} appGuid - Application GUID
     * @return {Promise<Array>} Array of structured log entries
     */
    getRecentParsed(appGuid) {
        return this.getRecent(appGuid).then(rawLogs => {
            if (!rawLogs || typeof rawLogs !== "string") return [];
            return Logs.parseLogs(rawLogs);
        });
    }

    /**
     * Parse raw CF log output into structured entries.
     * @param {String} rawLogs - Newline-separated log string
     * @return {Array} Array of { timestamp, timestampRaw, source, sourceId, messageType, message }
     * @static
     */
    static parseLogs(rawLogs) {
        if (!rawLogs || typeof rawLogs !== "string") return [];

        const lines = rawLogs.split("\n").filter(line => line.trim().length > 0);
        const logPattern = /^(\S+)\s+\[([^\]]+)\]\s+(OUT|ERR)\s+(.*)$/;

        return lines.map(line => {
            const match = line.match(logPattern);
            if (match) {
                const [, timestampRaw, sourceInfo, messageType, message] = match;
                const sourceParts = sourceInfo.split("/");
                const source = sourceParts[0] || "";
                const sourceId = sourceParts.slice(1).join("/") || "0";
                
                // Enhanced timestamp parsing for RFC3339/ISO8601 formats
                let timestamp = null;
                try {
                    // Handle RFC3339 with timezone (e.g., 2024-01-15T14:30:00.123Z or 2024-01-15T14:30:00+01:00)
                    // Handle ISO8601 formats (e.g., 2024-01-15T14:30:00)
                    // Also handles epoch milliseconds and other standard formats
                    const parsed = new Date(timestampRaw);
                    if (!isNaN(parsed.getTime())) {
                        timestamp = parsed;
                    }
                } catch (_) { /* keep null */ }
                
                return { timestamp, timestampRaw, source, sourceId, messageType, message: message.trim() };
            }
            return {
                timestamp: null, timestampRaw: null,
                source: "UNKNOWN", sourceId: "0",
                messageType: "OUT", message: line.trim()
            };
        });
    }
}

module.exports = Logs;