Have launchctl output stdout/stderr from the application to the system-wide Unified Logging mechanism

launchdlogs

I have set up a custom .plist in ~/Library/LaunchAgents to run a process (offlineimap) on a periodic basis. I've specified both the StandardOutPath and StandardErrorPath keys to point to files in my home directory, which works. However, this doesn't really provide a complete logging solution from what I can see. The logs are not rotated, there are no timestamps (addressed by this question, although there was no clear answer), and the log is not integrated into any other logging on the system. I think I would prefer it if the logs went to Unified Logging.

I'm used to systemd logging on Linux, which is a similar mechanism, and it would be great if there was a similar way I could redirect that output into the system-wide log, viewable with the log command. Is there any way to do that with a custom LaunchAgent?

I should add that I'm using MacOS 10.14.5.

Best Answer

You could use blowhole.

blowhole is a command-line tool that takes a string as an argument and sends it to the unified logging system. It is supported from Sierra (10.12) up to Catalina (10.15).

How to use it

(Tested on macOS Catalina 10.15.5)

Modify the ProgramArguments array in your .plist file like this:

<key>ProgramArguments</key>
<array>
    <string>/bin/bash</string>
    <string>-c</string>
    <string>COMMAND OPTIONS 2> >(while read; do /path/to/blowhole -e "$REPLY"; done) | while read; do /path/to/blowhole -d "$REPLY"; done</string>

where COMMAND OPTIONS is the command you want to execute, followed by any desired options.

Here, I make use of bash's support for redirection (2>), process substitution (>()) and pipelines (|) to:

  • sort out the command's standard error and standard output
  • process them separately inside two while loops. The first while loop runs blowhole -e to log standard error with an "Error" level:

    <timestamp> Error <info> blowhole: [co.eclecticlight.blowhole:general] Blowhole: STANDARD ERROR MESSAGES

    and the second one runs blowhole -d to log standard output with a "Default" level:

    <timestamp> Default <info> blowhole: [co.eclecticlight.blowhole:general] Blowhole: STANDARD OUTPUT MESSAGES

    (Since blowhole can't read from standard input, we need while loops to feed it a line of input at a time.)

The blowhole: [co.eclecticlight.blowhole:general] Blowhole: string is not configurable, but you can add a prefix of your choice to the logged messages. For example, since you mention offlineimap in your question:

/path/to/blowhole -d "offlineimap stdout: $REPLY";

and:

/path/to/blowhole -e "offlineimap stderr: $REPLY";

You can read log entries with sudo log show | grep blowhole:general or sudo log show | grep offlineimap, if you added the customized prefix. To read log entries as they are generated, in a manner similar to tail -f, use show stream instead.

Alternatively, you can wrap the command you want to execute in a shell script so that blowhole logs the command's standard output and error in a way similar to above. This is convenient if you want to run some code prior to executing the actual command:

#!/bin/bash
# Add the code you want to execute prior to the actual command here
COMMAND ARGUMENTS \
> >(
while read; do 
    /path/to/blowhole -d "$REPLY"; 
done) \
2> >(
while read; do 
    /path/to/blowhole -e "$REPLY"; 
done)

You can then configure the ProgramArguments array of your .plist file to run the script instead of your command:

<key>ProgramArguments</key>
<array>
    <string>/bin/bash</string>
    <string>/path/to/script.sh</string>
</array>

Where to get it from

You can download blowhole from its project page or directly from here. The program is provided as a signed, hardened and notarized executable (as required by Catalina) and as an Installer package.