Parsing And Pretty-Printing JSON Values On-The-Fly In Loggly Using Bookmarklets
In August, I'm helping to run an internal workshop on "Debugging With Logs". In the presentation, one of my closing thoughts is that you - as a software engineer - can always make your logging tools better by injecting a little custom CSS or JavaScript. In the past, I've looked at injecting CSS in order to fix Kibana's type-ahead bug. But, InVision has since moved on to using Loggly as our log aggregator. So, I thought it would be fun to share the bookmarklets that I've been using for Loggly. These revolve around formatting search results and parsing JSON (JavaScript Object Notation) on-the-fly.
Bookmarklets have two distinct flavors. They can either be entirely self-contained pieces of JavaScript that execute on the host page. Or, they can inject other resources into the host page that will evaluate as Script or Style tags. Using self-contained JavaScript has the benefit of being instantaneously effective. But, they are hard to write and have a size limitation. Injected resources, on the other hand, have a delay (due to network latency); but, they can be any size and are much easier to read and to write.
For Loggly, I've decided to go with the latter approach because the logic was too complex to keep in a small, self-contained Bookmarklet. Of course, in order to use the latter approach, I needed to host my bookmarklet scripts somewhere. So, I created a small GitHub project that can document my bookmarklets and serve up the script files (using a "gh-pages" branch).
With the script-injection approach, the actual bookmarklet itself just becomes a "javascript:" snippet that loads a given URL (from my GitHub project) into a Script tag and appends the Script tag to the host page. The non-minified version of this script looks something like this:
javascript:(function(d,b,f,u,s){
f = "loggly-search-formatting.js";
// Build the full URL, adding a cache-busting query-string parameter.
u = ( b + f + "?="+Date.now() );
// Create the SCRIPT element to inject and inject into the HOST page.
s = d.createElement( "script" );
s.setAttribute( "src", u );
d.body.appendChild( s );
})( document, "https://bennadel.github.io/Bookmarklets/" );
void(0);
The only variation here, from bookmarklet to bookmarklet, is the filename being interpolated into the URL. The rest is the construction and injection mechanism.
Once I had this in place, I went about creating script files that would augment my Loggly experience. First, I created a simple file that would do nothing more than inject some additional CSS to make the Loggly search results a bit more human-friendly:
(function() {
var head = $( "head:first" );
var style = $( "<style></style>" ).attr( "type", "text/css" );
// Setup the CSS styles to be used by the bookmarklet.
var styles = `
.ui-grid-cell-contents {
background-color: #FFFFFF ;
border-bottom: 1px solid #F0F0F0 ;
color: #000000 ;
font-family: 'Lucida Console', Monaco, monospace ;
font-size: 13px ;
padding: 11px 11px 11px 11px ;
}
.grid-view.ui-grid .ui-grid-cell[ ui-grid-cell ] {
height: 40px ;
}
`;
// Add all the nodes to the active documents.
head.append( style.html( styles ) );
})();
With this bookmarklet, the Loggly Grid display goes from this:
... with a low-contrast, cramped display to a high-contrast, padded - Kibana inspired - design:
Ah, much nicer. Much more readable (in my opinion, of course).
The Loggly search formatting bookmarklet was an easy one to write. But, now that you see the power of bookmarklet technology, let's get a little more complicated. In the next bookmarklet, I want to be able to double-click on a cell in the Grid layout and have it open up a modal window that contains the original JSON (JavaScript Object Notation) content of the Grid cell displayed in a pretty-printed format.
To do this, we need to inject some additional User Interface (UI) elements onto the page (to represent the modal window); and, we need to attach some event-delegation handlers to manage the openning and closing of said UI elements.
(function() {
// CAUTION: We can use jQuery in this bookmarklet because we know that Loggly
// uses jQuery in their application.
var doc = $( document );
var win = $( window );
var head = $( "head:first" );
var body = $( document.body );
var outer = $( "<div></div>" ).addClass( "bnb-outer" );
var inner = $( "<pre></pre>" ).addClass( "bnb-inner" );
var style = $( "<style></style>" ).attr( "type", "text/css" );
// Setup the CSS styles to be used by the bookmarklet.
var styles = `
.bnb-outer {
background-color: rgba( 0, 0, 0, 0.8 ) ;
bottom: 0px ;
display: none ;
left: 0px ;
position: fixed ;
right: 0px ;
top: 0px ;
z-index: 999999999999 ;
}
.bnb-outer--active {
display: block ;
}
.bnb-inner {
background-color: #FFFFFF ;
border: 1px solid #CCCCCC ;
border-radius: 7px 7px 7px 7px ;
bottom: 100px ;
color: #000000 ;
font-family: monospace ;
font-size: 18px ;
left: 100px ;
line-height: 27px ;
margin: 0px 0px 0px 0px ;
overflow: auto ;
padding: 30px 30px 30px 30px ;
position: absolute ;
right: 100px ;
top: 100px ;
white-space: pre-wrap ;
}
.bnb-inner strong {
color: #AA0000 ;
font-weight: 400 ;
}
`;
// When the user double-clicks in the Grid cell, check for valid JSON and pretty-
// print it in a modal window.
doc.on(
"dblclick",
".ui-grid-cell-contents",
function handleDblClick( event ) {
// Since there's no native way to check to see if a value is valid JSON
// before parsing it, we might as well just try to parse it and catch
// any errors.
try {
var node = $( this );
// Parse the JSON content and then re-stringify it so that it is
// formatted for easier reading.
var payload = JSON.parse( node.text() );
var content = JSON.stringify( payload, null, 4 );
// Inject the value into the modal window using .text() so that any HTML
// that might be embedded in the JSON is escaped.
inner.text( content );
// Pull the HTML content out, which now contain ESCAPED embedded HTML if
// it exists. At this point, we can do some light string-manipulation to
// add additional HTML-based formatting.
var html = inner
.html()
// Replace escaped line-breaks in strings with actual line-breaks
// that indent based on the prefix of the JSON key-value pair.
.replace(
/(^\s*"[\w-]+":\s*")([^\r\n]+)/gm,
function( $0, leading, value ) {
var indentation = " ".repeat( leading.length );
var formattedValue = value.replace( /\\n/g, ( "\n" + indentation ) );
return( leading + formattedValue );
}
)
// Emphasize the JSON key.
.replace( /("[\w-]+":)/g, "<strong>$1</strong>" )
;
inner.html( html );
// Show the modal window.
outer.addClass( "bnb-outer--active" );
} catch ( error ) {
console.warn( "Could not parse JSON in Grid cell." );
// console.log( error );
}
}
);
// When the user clicks on the outer portion of the modal window, close it.
doc.on(
"click",
".bnb-outer",
function handleClick( event ) {
// Ignore any click events from the inner portion of the modal.
if ( outer.is( event.target ) ) {
outer.removeClass( "bnb-outer--active" );
}
}
);
// When the hits ESC, close the modal window.
win.on(
"keydown",
function handleKeyPress( event ) {
var ESC_CODE = 27;
if ( outer.is( ".bnb-outer--active" ) && ( event.which === ESC_CODE ) ) {
outer.removeClass( "bnb-outer--active" );
}
}
);
// Add all the nodes to the active documents.
head.append( style.html( styles ) );
body.append( outer.append( inner ) );
})();
As you can see, the logic in these "bookmarklets" can start to get quite complex. Luckily, Loggly uses jQuery, which means that we can use the jQuery library directly in our Loggly-specific bookmarklets, making the logic less complex than it would otherwise need to be.
That said, if we run this bookmarklet on the Loggly page and then double-click one of the Grid cells that contains valid JSON, it will open up a modal window that looks like this:
NOTE: This is much easier to see in the Video demo.
Aside from being fun to create, Bookmarklets can be incredibly powerful. As you can see in this post, they allow us to augment the core behavior of the host page, adding and altering behaviors that make our lives easier. They are, of course, somewhat brittle and are bound to break as the host page evolves. But, in my experience, this happens much less frequently than you might think.
Want to use code from this post? Check out the license.
Reader Comments
What would be the advantage of this over let's say an extension written in js? Seems like it'd be easier to write complicated code if you can structure it.
@Yuriy,
At their core, both approaches are exactly the same. Though, to be fair, I don't have any experience writing JavaScript extensions. But, I think it's very similar, but the extension gives you more ?hooks? maybe into the host page life-cycle. Also, I think an extension might have more sandboxing. With the bookmarklet approach, you have to manually trigger the injection of code. This has drawbacks (you have to do it manually), and advantages (you have to do it manually) ;)
There are JavaScript extensions that do nothing but run user-defined snippets of code when you hit a given URL pattern. Which is basically like the bookmarklet idea, except it gets applied automatically rather than manually.
I guess I kind of like this approach because I own the whole process and the code runs as part of the host page. ... also, I don't have to learn anything new :D
@Ben, it's not so bad learning it. It's mostly making sure that your manifest.json file is right. It sounds intimidating until you look at it =c) Here is one I built: https://github.com/Pyro979/searchNdestroy