AEM 6 SP2 - View (Tail) CQ Log Files

Goal


Quick post on creating a Tail Log servlet for examining the logs on remote CQ instances; if you are debugging issues on a friend's CQ instance or QA, deploying this servlet could be helpful. Ootb, logs can be viewed by logging into felix console (http://localhost:4502/system/console/status-slinglogs) or doing a remote login (ssh) or http://localhost:4502/bin/crxde/logs?tail=1000

Demo | Package Install | Source Code

View error log : http://localhost:4502/bin/experience-aem/tail/log

View specific log (eg. access.log) : http://localhost:4502/bin/experience-aem/tail/log?log=access

Clear: Clears the current log view

Color Line Begin - First few characters of the line are colored (next fetch)

Start Line At - Cut out the first few characters of line, say timestamp (next fetch)

Line Min Length - Only the lines with length greater than entered number are returned (next fetch)

Line Max Length - Only the lines with length less than entered number are returned (next fetch)

Trim to Size - Cut the line to specified size (next fetch)

If Line Contains - Get line only if it contains the entered string (next fetch)

Not If Line Contains - Get line only if it does not contain entered string (next fetch)


Solution


Create an OSGI servlet apps.experienceaem.taillogs.TailLogsServlet with the following code

package apps.experienceaem.taillogs;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import java.io.*;
import java.util.Dictionary;
import org.apache.commons.lang3.StringEscapeUtils;

@Component(
        metatype = true,
        label = "Experience AEM Tail Logs",
        description = "Experience AEM Tail Logs Servlet")
@Service
@Properties({
        @Property(name = "sling.servlet.methods", value = {"GET", "POST"}, propertyPrivate = true),
        @Property(name = "sling.servlet.paths", value = "/bin/experience-aem/tail/log", propertyPrivate = true)})
public class TailLogsServlet extends SlingAllMethodsServlet {
    private final Logger LOG = LoggerFactory.getLogger(getClass());

    @Reference
    protected SlingSettingsService slingSettings;

    @Property(name = "logs.path", label = "Logs folder location",
            value = "",
            description = "Absolute path of log files")
    private static final String LOG_PATH = "log.path";

    @Property(name = "bytes.to.read", label = "Initial Bytes to Read",
            value = "2048",
            description = "The initial bytes to read")
    private static final String DEFAULT_BYTES_TO_READ = "bytes.to.read";

    @Property(name = "refresh.interval.millis", label = "Refresh Interval",
            value = "5000",
            description = "Log textarea refresh interval in millis")
    private static final String REFRESH_INTERVAL = "refresh.interval.millis";

    private String logFolderPath = null;
    private String bytesToRead = null;
    private String refreshInterval = null;

    @Activate
    protected void activate(final ComponentContext context) {
        Dictionary<String, Object> props = context.getProperties();
        Object prop = props.get(LOG_PATH);

        if(prop == null){
            logFolderPath = slingSettings.getSlingHomePath() + File.separator + "logs" + File.separator;
        }else{
            logFolderPath = String.valueOf(prop);
        }

        bytesToRead = String.valueOf(props.get(DEFAULT_BYTES_TO_READ));
        refreshInterval = String.valueOf(props.get(REFRESH_INTERVAL));

        LOG.info("Logs path : " + logFolderPath + ", Initial bytes to read : " + bytesToRead + ", Refresh interval : " + refreshInterval);
    }

    private void addLastNBytes(SlingHttpServletRequest request, SlingHttpServletResponse response, String logName)
                                    throws Exception{
        String filePointer = request.getParameter("pointer");
        String startLineAt = request.getParameter("startLineAt");
        String lineMinLength = request.getParameter("lineMinLength");
        String lineMaxLength = request.getParameter("lineMaxLength");
        String lineContains = request.getParameter("lineContains");
        String notLineContains = request.getParameter("notLineContains");
        String colorLineBegin = request.getParameter("colorLineBegin");
        String trimToSize = request.getParameter("trimToSize");

        File file = new File(logFolderPath + logName);

        long _filePointer = -1, len;
        int btr = Integer.parseInt(this.bytesToRead);

        if(StringUtils.isEmpty(filePointer)){
            _filePointer = file.length() - btr;
        }else{
            _filePointer = Long.parseLong(filePointer);

            len = file.length();

            //roll over or log clean
            if( len < _filePointer){
                _filePointer = len - btr;
            }
        }

        if(_filePointer < 0){
            _filePointer = 0;
        }

        StringBuilder sb = new StringBuilder();

        //based on //http://www.jibble.org/jlogtailer.php
        RandomAccessFile raf = new RandomAccessFile(file, "r");

        try{
            raf.seek(_filePointer);

            String line = null; int startAt = 0;

            if(StringUtils.isNotEmpty(startLineAt)){
                startAt = Integer.parseInt(startLineAt);
            }

            while ((line = raf.readLine()) != null) {
                if(startAt > 0 ){
                    if(line.length() > startAt){
                        line = line.substring(startAt);
                    }else{
                        continue; //skip lines shorter than desired
                    }
                }

                if(StringUtils.isNotEmpty(lineMinLength) && line.length() < Integer.parseInt(lineMinLength)){
                    continue;
                }

                if(StringUtils.isNotEmpty(lineMaxLength) && line.length() > Integer.parseInt(lineMaxLength)){
                    continue;
                }

                if(StringUtils.isNotEmpty(lineContains) && !line.contains(lineContains)){
                    continue;
                }

                if(StringUtils.isNotEmpty(notLineContains) && line.contains(notLineContains)){
                    continue;
                }

                if(StringUtils.isNotEmpty(trimToSize)){
                    line = line.substring(0, Integer.parseInt(trimToSize));
                }

                if(StringUtils.isNotEmpty(colorLineBegin) && colorLineBegin.equals("true")){
                    int length = (line.length() < 10) ? 1 : 10;
                    line = "<span style='color:red;font-weight:bold'>" + StringEscapeUtils.escapeHtml4(line.substring(0,length))
                            + "</span>" + StringEscapeUtils.escapeHtml4(line.substring(length));
                }

                sb.append(line);
            }

            _filePointer = raf.getFilePointer();

            raf.close();
        }catch(Exception e){
            raf.close();
            throw new ServletException("Error reading file - " + logName);
        }

        PrintWriter pw = response.getWriter();

        if(StringUtils.isEmpty(filePointer)){
            response.setContentType("text/html");

            String interval = Integer.parseInt(this.refreshInterval)/1000 + " secs";

            pw.write("<html><head><title>CQ Tail Log</title><script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js'></script><style>input[type='text']{width:50px}</style></head><body><div style='border: 1px solid; padding: 5px; height: 780px; overflow: scroll;'><code contenteditable='true' id='logData'>" + sb.toString() + "</code></div>");
            pw.write("<div>Log file : " + (logFolderPath + logName) + "</div>");
            pw.write("<div>Refreshing : <span id=status style='color:red'>" + interval + "</span> ");
            pw.write("| <input type=button value='pause' onclick=\"eaemTL.paused = !eaemTL.paused; this.value=eaemTL.paused ? 'resume' : 'pause'; $('#status').html(eaemTL.paused ? 'paused' : eaemTL.interval)\"/> ");
            pw.write("| <input type=button value='clear' onclick=\"$('#logData').html('');\"/> ");
            pw.write("| Color Line Begin : <input type=checkbox onchange='eaemTL.colorLineBegin=this.checked; updateTextArea()'/> ");
            pw.write("| Font Size : <input type=text onchange=\"$('#logData').css('font-size', this.value)\"> Px ");
            pw.write("| Start Line At : <input type=text onchange='eaemTL.startLineAt=this.value; updateTextArea()'> ");
            pw.write("| Line Min Length : <input type=text onchange='eaemTL.lineMinLength=this.value; updateTextArea()'> ");
            pw.write("| Line Max Length : <input type=text onchange='eaemTL.lineMaxLength=this.value; updateTextArea()'> ");
            pw.write("| Trim to Size : <input type=text onchange='eaemTL.trimToSize=this.value; updateTextArea()'> </div>");
            pw.write("<div>If Line Contains : <input type=text onchange='eaemTL.lineContains=this.value; updateTextArea()' style='width:600px'>      ");
            pw.write(" |     Not If Line Contains : <input type=text onchange='eaemTL.notLineContains=this.value; updateTextArea()' style='width:600px'></div> ");
            pw.write("<script type='text/javascript'>var eaemTL = { log: '" + logName + "', pointer : " + _filePointer + ", paused : false, interval : '" + interval + "' }; var $logData = $('#logData');");
            pw.write("function updateTextArea() { if(eaemTL.paused){return;} $.ajax( { url: '/bin/experience-aem/tail/log', data: eaemTL } ).done(function(data){ if(data.log){$logData.html($logData.html() + data.log)}; eaemTL.pointer = data.pointer});} setInterval(updateTextArea, 5000);</script></body></html>");
        }else{
            response.setContentType("application/json");

            JSONObject json = new JSONObject();
            json.put("log", sb.toString());
            json.put("pointer", _filePointer);

            json.write(pw);
        }
    }

    /**
     * @param request
     * @param response
     * @throws javax.servlet.ServletException
     * @throws java.io.IOException
     */
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
                        throws ServletException, IOException {
        try{
            String logName = request.getParameter("log");

            if(StringUtils.isEmpty(logName)){
                logName = "error";
            }

            if(!logName.endsWith(".log")){
                logName = logName + ".log";
            }

            addLastNBytes(request, response, logName);
        }catch(Exception e){
            LOG.warn("Error tailing logs servlet", e);
        }
    }
}

2 comments:

  1. Hi Sreekanth,
    This is working fine in AEM 6 but in 5.6 bundle fails to resolve the sling.setting[1.3,2] package. So pom need to updated make it work in 5.6 with proper range of setting class.
    Thanks,
    Rajeev

    ReplyDelete