All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.consoleplugin.logviewer.static.script.js Maven / Gradle / Ivy

/**
 * Copyright 2013 the original author or authors.
 *
 * 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.
 **/
$(function () {
    /**
     * Polyfills
     */
    (function(){
        if (!String.prototype.endsWith) {
            String.prototype.endsWith = function(search, this_len) {
                if (this_len === undefined || this_len > this.length) {
                    this_len = this.length;
                }
                return this.substring(this_len - search.length, this_len) === search;
            };
        }

        if (window.NodeList && !NodeList.prototype.forEach) {
            NodeList.prototype.forEach = Array.prototype.forEach;
        }
    })();

    /**
     * Continuously update the server time view with the actual backend time.
     */
    (function() {
        var failures = 0;
        var timerId = window.setInterval(function() {
            $.ajax({
                url: "logviewer/serverTime",
                dataType: "json",
                global : false,
                error: function() {
                    if (++ failures > 10) {
                        // Bail after 10 successive error responses from the server.
                        window.clearInterval(timerId);
                    }
                },
                success: function(data) {
                    failures = 0;
                    $("#serverTime").text(data.time);
                }
            });
        }, 1000);
    })();

    var KEY_ENTER = 13,
        KEY_A = 65,
        NEWLINE = /\r?\n/,
        LINES_PER_MB = 14000, // approximate number of log viewer lines per MB log data.
        PATTERN_SECTION_START = /\*(TRACE|DEBUG|INFO|WARN|ERROR)\*/,
        PATTERN_REQUEST_START = /^(.* )\[(\d+)]( -> (GET|POST|PUT|HEAD|DELETE) .*)$/,
        PATTERN_REQUEST_END = /^(.* )\[(\d+)]( <- \d+ .*)$/,
        textDecoder = new TextDecoder("UTF-8"),
        tailSocket,
        tailDomNode = document.getElementById("tail"),
        $logfile = $("#logfile"),
        $amount = $("#amount"),
        $followButton = $("#followButton"),
        $downloadCurrentLogfile = $("#downloadCurrentLogfile"),
        $downloadAllLogfiles = $("#downloadAllLogfiles"),
        $grep = $("#grep");

    /**
     * Enable chosen-select on the logfile dropdown.
     */
    $logfile.chosen({width : "16em"});

    /**
     * Enable download behaviors for the download buttons.
     */
    $downloadCurrentLogfile.click(function() {
        if ($logfile.val()) {
            window.open("logviewer/download?file=" + $logfile.val());
        }
        return false;
    });
    $downloadAllLogfiles.click(function() {
        window.open("logviewer/download");
        return false;
    });

    /**
     * Represents the Tail view and the related operations.
     */
    var Tail = {
        /**
         * The known types of logfiles.
         */
        LogType: Object.freeze({
            ERROR_LOG: 0,
            REQUEST_LOG: 1,
            ACCESS_LOG: 2
        }),

        /**
         * The regular expression to apply to all log entries (hide elements not matching)
         */
        filterExpression : undefined,

        /**
         * The amount of entries in the tail view to keep.
         */
        scrollBackBuffer : (parseFloat($amount.val()) || 0.1) * LINES_PER_MB,

        /**
         * The currently tailed log file type.
         */
        logType: undefined,

        /**
         * The buffered remained of a line not yet terminated.
         */
        buffer: "",

        /**
         * The currently open section (logically connected lines of a logfile, e.g. a stacktrace), if any.
         */
        currentSection: undefined,

        followMode: false,

        /**
         * Clears the log excerpt and re-sets any associated state.
         */
        clear: function () {
            tailDomNode.innerHTML = "";
            this.buffer = "";
            this.currentSection = undefined;
        },

        /**
         * Send meta-info about the tail process itself to the tail, e.g. a notification
         * when log file rotation occurs.
         */
        info: function (text) {
            tailDomNode.appendChild(document.createElement("br"));
            tailDomNode.appendChild(
                document.createTextNode(' ----------------- ' + text + ' ----------------- ')
            );
            tailDomNode.appendChild(document.createElement("br"));
            tailDomNode.appendChild(document.createElement("br"));
            this.followUp();
        },

        /**
         * Add a message to the tail.
         */
        add: function (text) {

            function linkRequestLog(match, config) {
                var link = document.createElement("a"),
                    div = document.createElement("div");

                link.textContent = '[' + match[2] + ']';
                config.attributes.forEach(function(attribute) {
                    link.setAttribute(attribute.name, attribute.value);
                });
                div.appendChild(document.createTextNode(match[1]));
                div.appendChild(link);
                div.appendChild(document.createTextNode(match[3]));
                div.appendChild(document.createElement("br"));
                return div;
            }

            var lines = (this.buffer + text).split(NEWLINE);

            for (var i = 0; i < lines.length - 1; ++i) {
                /**
                 * Convert each line of text into a new text DOM node. This non-normalized approach
                 * (many small text nodes instead of one large text node) has many advantages: It significantly increases performance
                 * as it allows the browser to only render the text nodes currently in view and allows to easily take single lines
                 * of text and highlight them, e.g. for highlighting errors.
                 *
                 * @type {Text}
                 */
                var textNode = document.createTextNode(lines[i]);
                var textContentForPatternMatching = textNode.nodeValue;

                // Enable section detection if we know we are in an error.log or the logfile type is not known.
                if (this.logType === Tail.LogType.ERROR_LOG || this.logType === undefined) {

                    // An interrelated entry, e.g. an error stack trace, was detected before and is not yet finished
                    if (this.currentSection) {
                        var firstChar = textNode.nodeValue.charAt(0);
                        // The first character is a tab or not a number -> consider it part of a stack trace.
                        if (firstChar === '\t' || (firstChar * 0) !== 0) {
                            // Add the node to the existing section
                            this.currentSection.appendChild(textNode);
                            this.currentSection.appendChild(document.createElement("br"));
                            // The section might be hidden as it did not yet contain a match for the grep expression.
                            // Make it visible in case the grep expression  matches after new data is added.
                            this.filterExpression &&
                                this.currentSection.style.display === "none" &&
                                this.filterExpression.test(this.currentSection.textContent) &&
                                (this.currentSection.style.display = "");
                            continue;
                        }
                        // The text is not part of the current section -> end the section
                        this.currentSection = undefined;
                    }

                    // A new interrelated section is detected.
                    var match = PATTERN_SECTION_START.exec(textContentForPatternMatching);
                    if (match !== null) {
                        this.logType = Tail.LogType.ERROR_LOG;
                        // Create a new div that will hold all elements of the logged entry, including stack traces
                        this.currentSection = document.createElement("div");
                        this.currentSection.className = match[1].toLowerCase();
                        // Add the current text to the newly created section
                        this.currentSection.appendChild(textNode);
                        this.currentSection.appendChild(document.createElement("br"));
                        // Add the newly created section to the log view
                        this.addToTail(this.currentSection);
                        continue;
                    }
                }

                if (this.logType === Tail.LogType.REQUEST_LOG || this.logType === undefined) {
                    var requestStartMatch = PATTERN_REQUEST_START.exec(textContentForPatternMatching);
                    if (requestStartMatch != null) {
                        this.logType = Tail.LogType.REQUEST_LOG;
                        var id = requestStartMatch[2];
                        var logLine = linkRequestLog(requestStartMatch, { attributes: [
                                {name : "href", value: '#res' + id},
                                {name : "name", value: 'req' + id}
                            ]});
                        logLine.id = 'req' + id;
                        logLine.nebaRequestId = 'res' + id;
                        logLine.onmouseover = function () {
                            var responseLine = document.getElementById(this.nebaRequestId);
                            this.title = responseLine ? responseLine.textContent : "No response for this request found in the current log excerpt.";
                        };

                        this.addToTail(logLine);
                        continue;
                    }

                    var requestEndMatch = PATTERN_REQUEST_END.exec(textContentForPatternMatching);
                    if (requestEndMatch != null) {
                        this.logType = Tail.LogType.REQUEST_LOG;
                        var id = requestEndMatch[2];
                        var requestLine = document.getElementById('req' + id);
                        var logLine = linkRequestLog(requestEndMatch, { attributes: [
                                {name : "href", value: '#req' + id},
                                {name : "name", value: 'res' + id}
                            ]});
                        logLine.id = 'res' + id;
                        logLine.title = requestLine ? requestLine.textContent : "No request for this response found in the current log excerpt.";

                        this.addToTail(logLine);
                        continue;
                    }
                }

                // Simply append the line
                var div = document.createElement("div");
                div.appendChild(textNode);
                this.addToTail(div);
            }

            if (lines[lines.length - 1]) {
                this.buffer = lines[lines.length - 1];
            } else {
                this.buffer = "";
            }

            this.followUp();
        },

        /**
         * Adds a new node to the tail view. removes old nodes with regards to the
         * scroll back buffer and calculates the nodes visibility based on the grep regex.
         */
        addToTail: function(node) {
            Tail.filterExpression && node.style && (node.style.display = Tail.filterExpression.test(node.textContent) ? "" : "none");
            tailDomNode.appendChild(node);
            // Limit number of entries to scrollback buffer
            while (tailDomNode.childNodes.length > this.scrollBackBuffer) {
                tailDomNode.childNodes.item(0).remove()
            }
        },

        updateFilterExpressionFromUserInput : function() {
            if ($grep.val() === "") {
                Tail.filterExpression = undefined;
                $grep.css("border", "1px solid transparent")
                    .attr("title", "");
                Tail.showAllTailEntries();
                return;
            }

            try {
                Tail.filterExpression = new RegExp($grep.val());
                Tail.applyFilterExpression();
                $grep.css("border", "1px solid green")
                    .attr("title", "")
            } catch (e) {
                Tail.filterExpression = undefined;
                $grep.css("border", "1px solid red")
                    .attr("title", "Invalid regular expression: " + e.message);
            }
        },

        /**
         * Applies a new or changed filter expression to all nodes
         * of the tail view.
         */
        applyFilterExpression: function() {
            var expression = Tail.filterExpression;
            expression && tailDomNode.childNodes.forEach(function(node) {
                node.style && (node.style.display = expression.test(node.textContent) ? "" : "none");
            });
        },

        showAllTailEntries : function() {
            tailDomNode.childNodes.forEach(function(node) {
                node.style && (node.style.display = "");
            });
        },

        /**
         * When follow mode is on, scroll to the bottom of the views.
         */
        followUp: function () {
            Tail.followMode &&
            (tailDomNode.scrollTop = tailDomNode.scrollHeight);
        },

        /**
         * Whether to follow the log file additions.
         * @returns {boolean} whether follow mode is on.
         */
        toggleFollowMode: function () {
            Tail.followMode = !Tail.followMode;
            if (Tail.followMode) {
                Tail.clear();
                followSelectedLogFile();
            } else {
                stopFollowing();
            }
            Tail.followUp();
            return Tail.followMode;
        },

        updateScrollbackBufferSize: function() {
            Tail.scrollBackBuffer = (parseFloat($amount.val()) || 0.1) * LINES_PER_MB;
        }
    };

    adjustViewsToScreenHeight();
    restrictCopyAllToLogView();

    try {
        tailSocket = createSocket();
        tailSocket.onopen = function () {
            initUiBehavior();
            updateFromRequestParameters();
        }
    } catch (e) {
        console && console.log(e);
        Tail.info("Unable to open server connection: " + e.message)
    }

    /**
     * Binds the log viewer behavior to the UI elements (such as buttons) once
     * a websocket connection was successfully established.
     */
    function initUiBehavior() {
        $logfile.change(function () {
            if ($logfile.val()) {
                logfileParametersChanged();
            }
            return false;
        });

        $grep.keydown(function (event) {
            return event.which !== KEY_ENTER;
        });

        $grep.keyup(function() {
            // De-bounce handling: Do not apply the filter expression upon
            // every character that is being typed, but a certain amount of time after
            // the last key press
            $grep.timer && window.clearTimeout($grep.timer);
            $grep.timer = window.setTimeout(Tail.updateFilterExpressionFromUserInput, 300);
        });

        Tail.updateFilterExpressionFromUserInput();

        $amount.change(Tail.updateScrollbackBufferSize);

        $followButton.click(function () {
            Tail.toggleFollowMode() ? activeStyle($followButton) : inactiveStyle($followButton);
            return false;
        });

        $amount.keydown(function (event) {
            if (event.which === KEY_ENTER) {
                logfileParametersChanged();
                return false;
            }
            return true;
        });
    }

    function activeStyle($button) {
        $button.css("background", "palegreen").css("font-weight", "bold")
    }

    function inactiveStyle($button) {
        $button.css("background", "").css("font-weight", "")
    }

    /**
     * Creates a new websocket and initializes message and error handling.
     *
     * @returns {WebSocket}
     */
    function createSocket() {
        var socket = new WebSocket((window.location.protocol === "https:" ? "wss" : "ws") + "://" + window.location.host + "/system/console/logviewer/tail");

        socket.onclose = function () {
            Tail.info("Connection to server lost. Trying to reconnect ...");
            window.setTimeout(function () {
                try {
                    tailSocket = createSocket();
                    tailSocket.onopen = function() {
                        Tail.clear();
                        tailSelectedLogFile();
                    };
                } catch (e) {
                    console && console.log(e);
                    Tail.info("Unable to open server connection: " + e.message);
                }
            }, 2000);
        };

        socket.onmessage = function (event) {
            var data = event.data;
            if (data instanceof ArrayBuffer) {
                Tail.add(textDecoder.decode(event.data));
            } else if (typeof data === 'string') {
                if ("pong" === data) {
                    return;
                }
                Tail.info(data);
            } else if (console) {
                console.error("Unsupported data format of websocket response " + data + ".")
            }
        };

        socket.binaryType = "arraybuffer";

        window.setInterval(function () {
            socket.readyState === WebSocket.OPEN &&
            socket.send("ping")
        }, 1000);

        window.onunload = function () {
            if (socket) {
                socket.onclose = undefined;
                socket.close();
            }
        };

        return socket;
    }

    /**
     * Load selected logfile from the request parameters, e.g. ?file=/my/file&amount=100
     */
    function updateFromRequestParameters() {

        var queryString = document.location.search;
        if (!queryString) {
            return;
        }

        var opts = {};

        var requestParameterPattern = /([?&])([^=]+)=([^&?]*)/g,
            match;

        while ((match = requestParameterPattern.exec(queryString)) !== null) {
            opts[match[2]] = decodeURI(match[3]);
        }

        opts.amount && (parseFloat(opts.amount) > 0) && $amount.val(opts.amount) && Tail.updateScrollbackBufferSize();

        if (opts.file) {
            $logfile.children().each(function (_, v) {
                if (v.value === opts.file) {
                    $logfile.val(opts.file);
                    $logfile.trigger("chosen:updated");
                }
            });
        }

        tailDomNode.innerHTML = "";
        tailSelectedLogFile();

        opts.grep && $grep.val(opts.grep) && Tail.updateFilterExpressionFromUserInput();
    }

    /**
     * Re-loads the page with the selected parameters.
     */
    function logfileParametersChanged() {
        var file = $logfile.val(),
            amount = parseFloat($amount.val()),
            grep = $grep.val(),
            href = document.location.href,
            queryPos = Math.max(href.indexOf("?"), href.indexOf("#")),
            endPos = queryPos === -1 ? href.length : queryPos;

        if (!(file && amount)) {
            return;
        }

        document.location.href = href.substr(0, endPos) + "?file=" + file + '&amount=' + amount + "&grep=" + (grep || "");
    }

    /**
     * Starts tailing the selected log file.
     */
    function tailSelectedLogFile() {
        var file = $logfile.val(),
            amount = $amount.val();

        if (!(file && amount)) {
            return;
        }

        tailSocket.send("tail:" + parseFloat(amount) + 'mb:' + file);
    }

    /**
     * Starts following the selected log file.
     */
    function followSelectedLogFile() {
        var file = $logfile.val(),
            amount = $amount.val();

        if (!(file && amount)) {
            return;
        }

        tailSocket.send("follow:" + parseFloat(amount) + 'mb:' + file);
    }

    /**
     * Stops following any logfile.
     */
    function stopFollowing() {
        tailSocket.send("stop");
    }

    function adjustViewsToScreenHeight() {
        tailDomNode.style.height = (screen.height * 0.65) + "px";
    }

    /**
     * Intercepts ctrl + a to create a text selection exclusively spanning the current
     * log data view.
     */
    function restrictCopyAllToLogView() {
        $(document).keydown(function (e) {
            if (e.keyCode === KEY_A && e.ctrlKey) {
                e.preventDefault();
                var range = document.createRange();
                range.selectNode(tailDomNode);
                var selection = window.getSelection();
                selection.removeAllRanges();
                selection.addRange(range);
            }
        });
    }
});




© 2015 - 2024 Weber Informatics LLC | Privacy Policy