 |
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 Digg DZone Furl 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
|