Pages

Friday, 30 November 2012

DTrace and JSON: Together at last!

In August of this year I jumped on a plane and moved from Australia to San Francisco to work for Joyent. It's been busy and exciting, from learning about a new city to finding my stride in a new role. I thought I'd take a moment to talk about a new DTrace feature that I just added to illumos-joyent, the core of our SmartOS operating system!

At Joyent we have been implementing JSON-formatted log files in some of our systems programming work, mostly through the node-bunyan logging framework for node.js applications. Formatting our log messages as JSON payloads affords us a substantially easier time of analysing and processing log records programmatically. Extending log files with more fields no longer requires rewriting the scripts that understand how to parse them — we can simply add new properties to the log records!

Bunyan, like every other modern logging framework, provides for a notion of 'log levels'. Trace- or debug-level log messages generally provide information helpful during development — or later troubleshooting — but are usually too large in volume, and too much of a performance hit, to be left enabled in production at all times. Enter DTrace USDT probes for logging, added recently to Bunyan by Bryan Cantrill. Now, the developer or operator can dynamically enable logging at any level in any running process and receive the generated log records in the form of arguments to DTrace probes.

The only argument passed to the Bunyan log probes when they fire is a JSON-formatted string payload, containing the object passed to Bunyan for logging by the application. While this new feature is a definite boon for developers and operators alike in being able to better understand the operation of the system, it becomes clear reasonably quickly that the string manipulation subroutines available for use within DTrace actions are not the friendliest way to filter on or format for output the contents of a JSON-formatted payload.

When Bryan suggested that we should provide a subroutine for JSON parsing in DTrace my interest was immediately piqued. The challenge of writing a compact, bespoke JSON parser to operating in the tightly restricted environment that is DTrace probe context was a rewarding one. The strict, well-defined nature of the JSON specification was an immediate and welcome asset in completing this work, which I put back yesterday.

The operation of the new json() subroutine is relatively straightforward. It accepts two arguments: a string containing a JSON payload and an element selector string describing which value to pull out of the JSON payload. Element selectors support:
  • simple string keys for objects, e.g. "key"
  • dot-separated keys for nested objects, e.g. "nested.object.key"
  • array indexing, e.g. "nested.object.array[5].key"

As a practical example, consider adding a http.ServerRequest object as the "req" property on an object that you then logged through Bunyan. First, we have to increase the DTrace strsize option to be large enough for the JSON payloads we'll be inspecting — in this example I've used four kilobytes. This D script would then filter on Bunyan log entries pertaining only to GET requests:

#pragma D option strsize=4k

bunyan*:::log-*
{
    this->j = copyinstr(arg0);
}

bunyan*:::log-*
/json(this->j, "req.method") == "GET"/
{
    printf("%s %s %s\n",
        json(this->j, "req.method"),
        json(this->j, "req.url"),
        json(this->j, "req.httpVersion"));
}

All values extracted by json(), including numeric values, are returned as a string precisely as they appeared in the original JSON. As part of this work, I also added a strtoll() subroutine to convert strings to integer types in D. This enables you to take a numeric value from a JSON payload representing something like latency or request size and aggregate these values using functions like sum() or quantize(). For example, if you were to add the HTTP response size to the object logged to Bunyan you could now trivially print out the sum of sizes of all responses once a second:
#pragma D option strsize=4k

bunyan*:::log-*
{
    this->j = copyinstr(arg0);
    @ = sum(strtoll(json(this->j, "response_size")));
}

tick-1s
{
    printa(@);
    trunc(@);
}

These new features are in the illumos-joyent gate right now, and should make their way into the next SmartOS bi-weekly build. In the (hopefully near!) future I will seek to integrate them into the upstream illumos-gate repository for other distributions to get access. My thanks to Bryan for guidance and code review, and to Joyent for being an awesome place to work!

Wednesday, 2 February 2011

Disable the Swoosh animation for Mac OS X Spaces

So, as it turns out there's a way to disable the motion sickness-inducing Swoosh! animation that happens when you switch between Spaces (virtual desktops) in Mac OS X. This appears to work on my 10.6.5 machine, but I haven't tested it anywhere else.

defaults write com.apple.dock \
workspaces-swoosh-animation-off \
-bool YES &&
killall Dock

Sunday, 17 October 2010

OpenIndiana Automated Install Server

This is a draft set of steps for getting an automated install server configured on almost any platform using only Apache, DHCP and TFTP. It's very rough at this point but it functions well enough to PXE boot and install a copy of OpenIndiana (OI).

First up, you should make a directory /export/install and:
git clone git://github.com/jclulow/illumos-misc.git /export/install


If you don't have git you can grab a tarball of the repository at github's web interface.

Until an OI bootable Automated Install (AI) ISO is available you can use the distro constructor to create your own. If someone wants to host a copy of a functional ISO that I've built, please let me know! I've made a few modifications to the AI ISO build descriptor that comes with OpenIndiana. You should grab the distro_const/ai_x86_image_JMC.xml file from the github repository and (on an OI 147 host) run:
distro_const build ai_x86_image_JMC.xml


After a while you'll get a usable ISO in /rpool/dc/media that you can use to set up the rest of your environment. You should extract the contents of the ISO into /export/install/ai_image. Assuming your TFTP server is rooted in /tftpboot you'll want to:
cp -r /export/install/ai_image/boot /tftpboot/oi


You'll also want a local IPS repository containing the current OI packages. Fetch this 2GB tarball: oi_147_spin2.tar.bz2. Extract it into /export/install/repo.

You can use rsync to bring the repo seed files you got from the tarball up to date, thus:
rsync -a pkg-origin.openindiana.org::pkgdepot-dev /export/install/repo/


In order to simulate parts of the automated install server that ships with OI I'm using a few CGI shell scripts. There are two ksh scripts and a list of packages to be installed (cgi-bin/PACKAGES_LIST) in the git repository which you can customise to your liking. I've also prepared some responses to the /versions/0 and /publisher/[01] methods of a real IPS repository server. As these responses are essentially static I'm just using regular text files.

Configure Apache (I used version 2.2 from pkgsrc) on your system. You'll need two virtual hosts, each listening on a different port (e.g. 5555 and 10000). These vhosts will map the various service URLs onto local repository content and the cgi scripts. They should be configured as per the sample in the git repo: doc/apache_vhost_config.txt.

Make sure you set the correct URL to the IPS repository vhost in environment variable $REPO_URL_MAIN in cgi-bin/ai-manifest.ksh. This tells the AI client to use your new local repository instead of the one on the Internet. Unlike the public URL, yours will not end in /dev if you've used the exact vhost configuration I've provided. Note that the additional /legacy repository is, by all accounts, incredibly large and you don't need many packages from it so I'm just using the public remote copy.

You should also create a GRUB menu.lst from the example in the git repository using the IP address and port numbers of your Apache vhosts and put it in /tftpboot/oi.

Finally, configure DHCP (I use ISC dhcpd) to answer your host's PXE requests. If you're also using ISC then something like this snippet should suffice:
...
option grubmenu code 150 = text;
...
# Force grubmenu to appear in the request list...
if exists dhcp-parameter-request-list {
option dhcp-parameter-request-list = concat(option dhcp-parameter-request-list,96);
}
...
host odin {
hardware ethernet 00:13:72:17:39:d2;
fixed-address 10.1.1.30;
next-server 10.1.1.10;
filename "oi/grub/pxegrub";
option grubmenu "oi/menu.lst";
}


With all this together you should be able to PXE boot a host with OI 147! Feedback and corrections welcome.

NB: Credit where it's due, I started with this page on the OpenIndiana Wiki.

Sunday, 20 June 2010

Forcing PXE Clients not to broadcast for extra DHCP options

We have a site-wide PXE boot setup that manages workstations everywhere (using Altiris). Occasionally I want to netboot a specific non-managed boot loader from a specific TFTP server just by configuring the DHCP options for that host.

By default the PXE client will request (via broadcast) an address. Our primary ISC DHCP server answers this request. The PXE specification, however, allows for additional DHCP servers that don't provide addresses but do provide boot options (i.e. TFTP server and filename) to clients. These additional options (coming from Altiris) override those provided in the original DHCP response by our primary ISC DHCP server.

After a quick read through the PXE specification I discovered this workaround to force a specific client to use the provided TFTP server/filename in the original DHCP response. PXE clients will accept (in the original DHCP response) a discovery control setting. You can use this to disable the secondary broadcast behaviour and force the PXE client to do as it was instructed by the primary DHCP server. This option is an encapsulated vendor option so we need to configure it in dhcpd.conf, thus:

# PXE Vendor Option Space:
option space PXE;
option PXE.discovery-control code 6 = unsigned integer 8;


Then, when defining a client you toggle on the appropriate bits:

host jmcdesk {
hardware ethernet 00:23:ae:61:13:d6;
next-server 10.10.10.10;
filename "pxelinux.0";
vendor-option-space PXE;
option PXE.discovery-control 11;
}


The option in question is PXE_DISCOVERY_CONTROL from the Preboot Execution Environment (PXE) Specification Version 2.1. The value eleven (11) informs the client to skip broadcast/multicast discovery and to use the boot filename from the original DHCP request.