Auditing Hooks (15R6)

Auditing in R6 is subject to change based on user feedback. Please let us know if this feature does not meet your requirements.

In order to support easier integration of Connexion (and gateway) audit information with 3rd party systems, R6 introduces new audit hooks. These hooks work in the same manner as Alerting and Monitoring hooks - we expose functions within custom code which can execute your code as audit event objects are generated.

Three categories of auditing have been added to Connexion:

  1. PHI Message stream auditing: Whenever a message is received or sent by Connexion, a “phi” audit object is created. The “phi” audit object potentially contains personal health information derived from the received (or sent) message. Specifically, the columns displayed in a queue device, such as sending facility, MRN, Account, Exam/Doc Id, keywords, source/target Uri etc.

  2. PHI User action auditing: Whenever a user interacts with, or alters, message data. Currently this is limited to users interacting with messages via the queue device UI. Actions such as querying, viewing, copying/pasting, deleting, altering, queueing etc. are filed here.

  3. Non-PHI User action auditing: User actions which are not related to messages. These are configuration changes and state changes (start/stop/pause etc.).

Since many deployments will not be interested in logging every message passing through Connexion, each of the above auditing types can be enabled or disabled. By default, all auditing is disabled.

Additionally, Connexion utilizes several buffering mechanisms (memory, disk) in order to minimize the impact of high-volume auditing.

Audit settings are accessed via the Auditing tab on the System Configuration screen:

At the top of the audit UI, you have checkboxes to enable each type of auditing. Most of the code shown above is a boilerplate example and it is expected that you will replace this with your own implementation.

The main methods are:

Start: called when the auditing sub-system starts (at process launch, or when the audit configuration changes). This is where you can initialize any class-level variables.

Stop: called immediately before the auditing sub-system stops (at process stop, or, when the audit configuration changes).

ProcessAuditRecordsAsync: called when one or more non-phi audit records are available for processing. The boilerplate example code shows the expected access pattern.

ProcessPhiAuditRecordsAsync: called when one or more phi audit records are available for processing. The boilerplate example code shows the expected access pattern.

There are two separate methods called depending on whether the audit record contains phi or not. This makes it easy to treat phi audit data distinctly from non-phi audit data (for example, you may be forwarding phi audit data to a different repository than non-phi data).

Notes:

Audit data is buffered both in memory and on disk. Audit data is written into files which are then consumed on a separate thread. These files are read once they contain a maximum number of audit records, or, they are at least 2 minutes old. This means that there can be a delay of up to 2 minutes and 30 seconds between an audit record being buffered to disk and the custom auditing hook being called.

Note that it is possible to consume the cached audit files via an entirely different application. If you are operating a very busy Connexion system and cannot spare the bandwidth to also process audit data, you can grab the cache files directly (the filename is exposed in the method arguments) and forward them to another location.

(Contrived) example:

 

using System; using System.Linq; using System.Text; using System.IO; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Connexion.Core; using Connexion.Share.Threading; namespace Connexion.Device { public class CustomAuditing : BaseCustomAuditing { /// <summary> /// This class allows you to provide custom workflow for your audit data. For example, you may wish to save your audit data into specific files, or, send your audit data to a /// service over the network. Use the Start() and Stop() methods to initialize and tear down class-level variables. Use the ProcessAuditRecordsAsync method to process non-phi /// audit data, and the ProcessPhiAuditRecordAsync method to process phi data. /// *** /// *** The ProcessAuditRecordsAsync and ProcessPhiAuditRecordAsync methods run in separate threads, which means you *must synchronize* these methods if you are writing to a /// *** single non-thread-safe resource (such as a file). Use of lock(...) is acceptable. /// *** /// If you only wish to copy the raw audit data to another location (and skip processing), you can use the reader.FilePath property. In this case you would need to deploy your /// own audit parsing and processing application. /// *** /// The below sample code is a non-production sample. /// </summary> private StreamWriter m_Writer; private readonly AsyncLockEx m_AsyncLock = new AsyncLockEx(); public override void Start() { // initialize any class-level resources } public override void Stop() { // tear-down any class-level resources CloseStream(); } private void CloseStream() { var temp = m_Writer; m_Writer = null; temp?.BaseStream?.Close(); } private void CreateStream() { if(m_Writer?.BaseStream?.Length < 10 * 1024 * 1024) return; CloseStream(); var targetPath = "c:\\temp\\audit\\"; Directory.CreateDirectory(targetPath); var filePath = $"{targetPath}audit_{DateTime.Now.ToString("yyyyMMddHHmmss")}.txt"; var stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); m_Writer = new StreamWriter(stream, Encoding.UTF8, 8 * 1024, false); } public override async Task ProcessAuditRecordAsync(IAuditReader<AuditRecord> reader, CancellationToken cancellationToken) { // Non-PHI data (your logic here, do something better than below) foreach(var batch in reader.EnumerateBatches(100)) { var builder = new StringBuilder(); foreach(var record in batch) { builder.AppendLine(record.ToString()); } await WriteToAuditRecord(builder.ToString()); } } public override async Task ProcessPhiAuditRecordAsync(IAuditReader<PhiAuditRecord> reader, CancellationToken cancellationToken) { // Phi data (your logic here, do something better than below) foreach(var batch in reader.EnumerateBatches(100)) { var builder = new StringBuilder(); foreach(var record in batch) { builder.Append(record.ToStringWithPhi()); } await WriteToAuditRecord(builder.ToString()); } } private async Task WriteToAuditRecord(string toWrite) { using(await m_AsyncLock.LockAsync()) { CreateStream(); await m_Writer.WriteAsync(toWrite); await m_Writer.FlushAsync(); } } } }

Device/Plugin developers can write audit events from within their own code by grabbing a handle to an AuditProvider. Assuming you have a message context, you can use the snippet below to write a record which will then be processed by the code above.

if (MessageChannel.AuditProvider?.IsPhiMessageFlowAuditingEnabled == true) { var ar = context.CreatePhiAuditRecord(AuditAction.Device.SendMessage); ar.TargetUri = // if you are sending to an endpoint, set this. Can be file, network... MessageChannel.AuditProvider.AuditPhi(ar); }