The Source for Java Technology Collaboration
User: Password:



Stephen Friedrich

Stephen Friedrich's Blog

Big Severe Logging with Ascii Art

Posted by skelvin on August 24, 2004 at 03:46 AM | Comments (7)

A little Java 2D can give your log messages much bigger visibility. Or find yourself your own excuse to have some fun with ascii art graphics using Java 2D...

 #########            ##  ##                             ##  
 #########            ##  ##                             ##  
 ##                       ##                             ##  
 ##           #####   ##  ##  ##    ##  ## ##   ####     ##  
 ##          #######  ##  ##  ##    ##  #####  ######    ##  
 ########   ##    ##  ##  ##  ##    ##  ###   ###  ##    ##  
 ########       ####  ##  ##  ##    ##  ##    ##    ##   ##  
 ##          #######  ##  ##  ##    ##  ##    ########   ##  
 ##         ####  ##  ##  ##  ##    ##  ##    ########   ##  
 ##         ##    ##  ##  ##  ##    ##  ##    ##         ##  
 ##         ##   ###  ##  ##  ##   ###  ##    ###   ##       
 ##         ########  ##  ##  ########  ##     ######    ##  
 ##          #### ##  ##  ##   #### ##  ##      ####     ##  

I consider myself a lucky guy, being allowed to work full-time with Java and get paid for it. Only occasionally I do miss the chance to step back a little from business demands and just write some cool, yet more or less useless code.

So I jumped to the occasion when a day's work was almost done and I had the mean idea of putting something cool to use: I just had spent some thirty minutes tracing a bug when I noticed that single message in the enormous log file that revealed useful information. I cursed myself for not noticing earlier that it was logged as "SEVERE" and caught myself thinking "Couldn't the fellow who put that log message in there have done it in 40 pixels big bold face?" Well, of course not, it's plain ascii text, ... but hey, wait - I can do that!

Those of you who have been around long enough probably remember the novelty services that accepted a small photo and handed back to you an ascii print out that let you recognize the photo - if you stepped back far enough for the black and white to get blurred into a gray distribution. We really have come a long way with real-time photo-realistic graphics and powerful APIs like Java 2D and 3D!

Well, right now I could use some very simple, black and white ascii art: Create an image, draw the string to it, get the pixel raster and print out a space for a white pixel and some other character for a black pixel. Ten minutes' job.

In fact, it turned out to be two hours fumbling with Java 2D and the logging system. But that was due to my partial knowledge of those APIs on the one hand, and my inherent desire to go beyond the quick and dirty initial implementation and deal correctly with line breaks, tabs and null text on the other hand. Well, at least I stopped short of trying to support right-to-left, bottom-to-top text. I'm leaving that as an exercise for the reader ;-)

Text to Ascii Art

So let's have a short walk through the most important parts of the code.
I'd like to have something like this:

public static String getAsciiArt(Font font, String text) {

Calling getAsciiArt(new Font("SansSerif", Font.PLAIN, 18), "Failure!") should give the string you see above.

On we go: Before you can do anything sensible with text as graphics, you need a FontRenderContext to specify exactly how a font is mapped to a pixel raster:

            FontRenderContext fontRenderContext = new FontRenderContext(null, false, false);

The output does not need to be transformed in any way (rotated, scaled, ...), so passing null gets me an IdentityTransform. The two false literals specify no anti-aliasing and non-fractional metrics (Does not make sense when you think of a pixel as a character, right?) The usual way to get a FontRenderContext instance is from the Graphics2D object passed into any paint() method of components, but this isn't the usual paint application, anyway.

Now before the text can be drawn into an image I need to know the image size. That resulted in some heavy application of the engineering pattern called trial and error:
Most methods (e.g. TextLayout.getBounds()) were not working as expected, e.g. ignoring white space at the text's start or end (which makes some sense, there's nothing to draw there). Finally I found that creating a GlyphVector and using its logical bounds did what I wanted:

                GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, text);
                Rectangle2D bounds      = glyphVector.getLogicalBounds();

Next, simply create an image to paint to:

                BufferedImage image = new BufferedImage((int) bounds.getWidth(), 
                                                        (int) bounds.getHeight(),
                                                        BufferedImage.TYPE_BYTE_BINARY);

Drawing into the image is quick, just get a graphics object from the image and use it to draw to the image:

                Graphics2D graphics = image.createGraphics();
                graphics.drawGlyphVector(glyphVector, 0, ascent);

I haven't explained the "ascent" yet, so what's that about? The drawGlyphVector() method does not document at all what the x/y position passed into it is relative to. I expected it to be the baseline, i.e. that line you write on in a lined text book with characters like "y" extending further down. That assumption was correct, but unfortunately the GlyphVector class has no easy way to retrieve the baseline.

Well, TextLayout seems to supply what I needed:

                TextLayout textLayout = new TextLayout(text, font, fontRenderContext);

Grmpf! textLayout.getBaseline() does not return the actual position of the baseline, but rather its type - a stupid name for this method. I only learned about different types of baselines after some unsuccessful attempts to position the text and I'd rather not elaborate on that here. Still, TextLayout has a nice method called getAscent(), which is just what I need - the delta from the top of the bounds to the baseline:

                float ascent = textLayout.getAscent();

What's left is to get the actual pixel raster and iterate over pixels appending characters to the log:

                    Raster raster = image.getRaster();
                    ...
                    raster.getPixel(x, y, ints);
                    int pixelValue = ints[0];
                    buffer.append(pixelValue > 0 ? '#' : ' ');

There actually is quite a bit more code, but you can have a look at that for yourself in the complete implementation below.

Logging in Ascii Art

We are using our own wrapper around log4j, but for this blog I'll stick to the standard Java logging mechanism (from java.util.logging).

Before any text is actually "published" (the log system's lingo for finally outputting it), it can be formatted. Well, that's nice because our ascii art output can be easily implemented as a Formatter. On second thought, I make that a Formatter that wraps another Formatter, so that the usual formatting options (which may include timestamp, thread name etc.) aren't lost.

So let's supply a base formatter instance plus a font used for ascii art and a minimum log level to begin logging in ascii art. Oh yeah, and let those arguments default to something sensible if nulls are passed:

public class AsciiArtFormatter extends Formatter {
    public AsciiArtFormatter(Formatter formatter, Level level, Font font) {
        this.formatter = (formatter == null ? new SimpleFormatter() : formatter);
        this.level     = level == null ? Level.SEVERE : level;
        this.font      = font == null ? new Font("SansSerif", Font.PLAIN, 18) : font;
    }
The one method that has to be implemented is, of course, format():
    public String format(LogRecord record) {
        String message = formatter.format(record);
        if(record.getLevel().intValue() >= level.intValue()) {
            message = message + getAsciiArt(font, record.getMessage());
        }
        return message;
    }

I have included the complete message (as obtained from the wrapper formatter's format() method) so that the text can be found if you search the log file using your favorite text editor and then append the ascii art string to it. The ascii art is indeed graphics - with quite low resolution - so that if only that were output, you wouldn't be able to find anything if you were searching for some important text.

Code

Finally, here's the complete code, starting with a simple usage example:
import java.io.File;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Main {
    public static void main(String[] args) throws IOException {
        String userHome    = System.getProperty("user.home");
        File   logFile     = new File(userHome, "bigSevereText.log");
        String logFilePath = logFile.getPath();
        System.out.println("Logging to file " + logFilePath);    

        Logger logger = Logger.getLogger("default");
        logger.setLevel(Level.FINEST);
        FileHandler logFileHandler = new FileHandler(logFilePath);
        logFileHandler.setFormatter(new AsciiArtFormatter());
        logger.addHandler(logFileHandler);

        logger.fine("Fox prepares to jump.");
        logger.info("Fox is ready to jump.");
        logger.severe("Fox failed\nto jump over\nlazy dog.");
        logger.info("Dog is chasing fox.");
    }
}

I have made the getAsciiArt() method part of the formatter, but you might like to move it to some other place (it is totally independent from logging after all):

import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import java.util.logging.Level;
import java.util.Arrays;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.geom.Rectangle2D;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextLayout;

public class AsciiArtFormatter extends Formatter {
    private final Formatter formatter;
    private final Font      font;
    private final Level     level;

    public AsciiArtFormatter() {
        this(null);
    }

    public AsciiArtFormatter(Formatter formatter) {
        this(formatter, null, null);
    }

    /**
     * Creates a formatter that will additionally use ascii art to log the message in the
     * given font if its log level is at least level.
     *
     * @param formatter used to format the message before appending ascii art message.
     *                  If null a default SimpleFormatter is used.
     * @param level     minimum level needed to enable ascii art logging.
     *                  If null Level.SEVERE is used.
     * @param font      font used for any ascii art output.
     *                  If null a sans serif font of size 18 is used.
     */
    public AsciiArtFormatter(Formatter formatter, Level level, Font font) {
        this.formatter = (formatter == null ? new SimpleFormatter() : formatter);
        this.level     = level == null ? Level.SEVERE : level;
        this.font      = font == null ? new Font("SansSerif", Font.PLAIN, 18) : font;
    }

    public String format(LogRecord record) {
        String message = formatter.format(record);
        if(record.getLevel().intValue() >= level.intValue()) {
            message = message + getAsciiArt(font, record.getMessage());
        }
        return message;
    }

    /**
     * @return the text as 'Ascii Art' in the given font using a '#' character for
     *         each pixel (the text itself if any runtime exception occurs)  
     */
    public static String getAsciiArt(Font font, String text) {
        try {
            if(text == null) {
                text = "(null)";
            }
            int tabWidth = 4;
            
            text = text.replaceAll("\\t", repeat(' ', tabWidth));
            FontRenderContext fontRenderContext = new FontRenderContext(null, false, false);
            StringBuffer      buffer            = new StringBuffer();
            String[]          lines             = text.split("\n|\r\n", 0);
            for(int lineIndex = 0; lineIndex < lines.length; lineIndex++) {
                // How stupid: TextLayout does not handle white space like I want to, and
                // GlyphVector has no convenient method to calculate overall ascent/descent.
                // So just use both...
    
                String      line        = lines[lineIndex];
                GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, line);
                Rectangle2D bounds      = glyphVector.getLogicalBounds();
                int         width       = (int) bounds.getWidth();
                int         height      = (int) bounds.getHeight();

                if(line.length() == 0) {
                    buffer.append(repeat('\n', height));
                    continue;
                }

                TextLayout    textLayout = new TextLayout(line, font, fontRenderContext);
                float         ascent     = textLayout.getAscent();
                BufferedImage image      = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
                Graphics2D    graphics   = image.createGraphics();
                
                try {
                    graphics.drawGlyphVector(glyphVector, 0, ascent);

                    Raster raster = image.getRaster();
                    int[] ints = new int[1];
                    for(int y = 0; y < height; ++y) {
                        for(int x = 0; x < width; ++x) {
                            raster.getPixel(x, y, ints);
                            int pixelValue = ints[0];
                            buffer.append(pixelValue > 0 ? '#' : ' ');
                        }
                        buffer.append('\n');
                    }
                }
                finally {
                    graphics.dispose();
                }

            }

            return new String(buffer);
        }
        catch(RuntimeException e) {
            // Don't want anything silly to happen only because using this fun method.
            // Revert to plain output ;-(
            return text;
        }
    }
    
    /**
     * @return a string consisting of the character c repeated count times.
     */
    public static String repeat(char c, int count) {
        char[] fill = new char[count];
        Arrays.fill(fill, c);
        return new String(fill);
    }
}

Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • Very nice

    ____ _ _
    / ___|___ ___ | | |
    | | / _ \ / _ \| | |
    | |__| (_) | (_) | |_|
    \____\___/ \___/|_(_)


    Posted by: johnreynolds on August 24, 2004 at 07:01 AM

  • A more boring solution
    A simpler but far less fun solution is to have two log files, one with the sensitivity set to fine, the other to severe, then you can find all the severe messages straight away

    Posted by: c_armstrong on August 24, 2004 at 09:36 AM

  • You da man!
    I always appreciate when my coworkers give me a "You da man!" for doing something like this. So...

    You da man!

    (Sorry, I didn't use your code to print that in #'s.)

    -Andy

    Posted by: detorres on August 24, 2004 at 11:07 AM

  • Another option - Chainsaw V2
    It's not easy to spot individual events in large log files, but the new version of log4j's Chainsaw could help.

    It's available via WebStart here:
    http://logging.apache.org/log4j/docs/chainsaw.html

    You can parse and tail a log file using the new LogFilePatternReceiver (regardless of where the log file was generated, as long as the pattern in the file is consistent).

    Once the events are loaded in Chainsaw, you can find ERRORs (for example) by using this expression in the 'refine focus' field:
    LEVEL >= ERROR

    Hopefully this can help cut down the time you have to spend finding events in the log file.

    Scott

    Posted by: sdeboy on August 24, 2004 at 01:48 PM

  • we use figlet for this...
    back in the day, the unix command line tool 'banner' did whats described in the article; someone wrote 'figlet' as a nicer banner, it can use a selection of hand-made ascii-art fonts. Now we're in the 21st century, there's loads of figlet-cgi gateways out there so you can just generate the art you want, eg:
    http://software.hixie.ch/utilities/cgi/figlet/figlet

    We have a couple of figlet banners in our log4j output, ever since one of the devs couldn't spot where an important process started up. Now it announces itself in style.

    Posted by: ba22a on August 25, 2004 at 02:00 AM

  • Grep?
    Fanci ASCII is all well and fine, but make sure you remember to include a "plain text" version of the ASCII Font Graphics, so you can do a simple "grep failure" on your logs.

    Posted by: ashleyherring on August 25, 2004 at 04:23 PM

  • Shallow or Hallow? Obvioiusly there are still some folks (like c_armstrong, sdeboy, ashleyherring) who just don't understand the concept of writing code for the pure enjoyment of being creative, clever or cool. They probably don't see the value of painting a masterpiece on the ceiling of a church either (not that we should start painting ASCII art on the ceilings of churches . . . or should we?)

    Posted by: paedagogus on June 22, 2006 at 09:33 AM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds