Can I have launchctl output stdout/stderr from my application to the system-wide Unified Logging mechanism?

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.