LAUREL BRIDGE

LaurelBridge.DCF.Examples.SessionLogging Namespace

DICOM Connectivity Framework V3.4
The SessionLogging example demonstrates a number of logging capabilities using the NLog adapter in DCF.
Classes

  ClassDescription
Public classProgram
Demonstrate session based logging and configuring a rotating log, works by creating a separate log file for each association.
Examples

SessionLogging Sample Code
public class Program
{
    private static readonly ILogger Logger = LogManager.GetCurrentClassLogger();

    /// <summary>
    /// Maintains the list of seen associations, identified by their session id.
    /// </summary>
    /// <remarks>
    /// This list is appended to in the callback store server's endAssociation.
    /// </remarks>
    private static readonly List<string> SessionIds = new List<string>();

    private const int NumberOfStoreJobs = 5;

    /// <summary>
    /// Main entry point for example.
    /// </summary>
    [STAThread]
    private static void Main()
    {
        AssociationManager mgr = null;
        try
        {
            LogManager.LogAdapter = new NLogLogAdapter();

            // Enable debugging for the default session
            Framework.DefaultSessionSettings.IsDebugEnabled = true;

            mgr = StartSCP(104);
            StoreSCU scu = new StoreSCU();

            for (int i = 0; i < NumberOfStoreJobs; i++)
            {
                Logger.InfoFormat("Submitting store job {0}...", i);
                scu.SubmitStoreJob(CreateStoreJob());
            }

            // Stop the SCP, waiting for any currently running associations to finish
            if (mgr != null && mgr.Running)
                mgr.Stop();

            // Flush any buffered log messages out to disk.
            LogManager.LogAdapter.Flush();

            // Get the log directory path from the nlog configuration in our app config
            string logDir = GetNLogLogDirectoryPath("SessionFileLogger");

            // Prompts the user to view selected log file contents
            ViewLogFiles(logDir);
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception caught during execution: {0}", e);
            // Shutdown the SCP, terminating any current associations
            if (mgr != null && mgr.Running)
                mgr.Shutdown();
        }

        if (!Debugger.IsAttached) return;
        Console.Write("Press any key to continue . . . ");
        Console.ReadKey();
    }

    private static StoreJobDescription CreateStoreJob()
    {
        StoreJobDescription sjd = new StoreJobDescription();
        sjd.CalledAETitle = "SCP";
        sjd.CallingAETitle = "SCU";
        sjd.CalledHost = "localhost";
        sjd.CalledPort = "104";
        sjd.AddInstance(new DicomInstanceInfo("mr-knee.dcm"));
        return sjd;
    }

    private static AssociationManager StartSCP(int port)
    {
        CallbackStoreServer storeServer = new CallbackStoreServer();
        AssociationManager mgr = new AssociationManager();
        mgr.ServerTcpPort = port;
        mgr.AssociationConfigPolicyMgr = storeServer;
        mgr.AddAssociationListener(storeServer);

        // AssociationManager.run() blocks, so start him listening for connections on a separate thread
        Thread t = new Thread(mgr.Run);
        t.Start();
        if (!mgr.WaitForRunning(2000))
        {
            throw new TimeoutException("AssociationManager did not start in an acceptable amount of time");
        }

        Console.WriteLine("SCP started and listening on {0}...", port);

        return mgr;
    }

    /// <summary>
    /// Prompts the user for an entry selection to view a given log file in notepad. The list of log files
    /// will reflect only log files whose session id matches one of the listed session id's encountered during
    /// the execution of this example.
    /// </summary>
    /// <param name="logDir">The output log directory where the log files were written to.</param>
    private static void ViewLogFiles(string logDir)
    {
        if (String.IsNullOrEmpty(logDir))
            throw new ArgumentException("invalid logDir");
        if (SessionIds == null || SessionIds.Count == 0)
            throw new InvalidOperationException("no session ids!");
        if (!Directory.Exists(logDir))
            throw new DirectoryNotFoundException(logDir);

        Dictionary<int, string> map = CreateLogFileMap(logDir);

        try
        {
            for (; ; )
            {
                Console.WriteLine("{0}Log output files in {1}:{0}", Environment.NewLine, logDir);
                for (int i = 0; i < map.Count; i++)
                {
                    Console.WriteLine("{0}: {1}", i, map[i]);
                }

                string selection;
                int entry;
                for (; ; )
                {
                    Console.Write("{0}Input index to view contents of log file (press 'q' to quit): ", Environment.NewLine);
                    selection = Console.ReadLine();
                    if (IsUserQuit(selection))
                        return;
                    if (Int32.TryParse(selection, out entry) && entry >= 0 && entry < map.Count)
                        break;
                    Console.WriteLine("Invalid entry.  Please re-enter selection...");
                }

                Console.WriteLine("Entry({0}): Contents of log file ({1}):", selection, map[entry]);
                Process.Start("notepad.exe", map[entry]);
            }
        }
        finally
        {
            Console.Write("Delete created log files from this example? (y or n): ");
            string answer = Console.ReadLine();
            if (answer != null && (answer.ToLower().Trim().Equals("y") || answer.ToLower().Trim().Equals("yes")))
            {
                if (map != null)
                {
                    Console.WriteLine("Deleting log files...");
                    foreach (string file in map.Values)
                    {
                        // we know the extension for this example is .log, just paranoia
                        if (file.EndsWith(".log"))
                            File.Delete(file);
                    }

                    string archiveDir = Path.Combine(logDir, "Archive");
                    if (Directory.Exists(archiveDir))
                        Directory.Delete(archiveDir, true);
                }
            }
        }
    }

    private static bool IsUserQuit(string txt)
    {
        if (txt == null)
            return false;
        txt = txt.ToLower().Trim();
        return txt == "q" || txt == "quit" || txt == "exit";
    }

    /// <summary>
    /// Creates a simple mapping between a selection number and a log file. Used to prompt the user for viewing a log file.
    /// </summary>
    /// <param name="logDir">The log directory.</param>
    /// <returns>The mapping for a given entry number and log file path.</returns>
    private static Dictionary<int, string> CreateLogFileMap(string logDir)
    {
        Dictionary<int, string> map = new Dictionary<int, string>();

        string defaultSessionLogFile = Path.Combine(logDir, Process.GetCurrentProcess().ProcessName + ".log");

        int selectionNumber = 0;
        map.Add(selectionNumber++, defaultSessionLogFile);
        foreach (string sessionId in SessionIds)
        {
            map.Add(selectionNumber++, Path.Combine(logDir, sessionId + ".log"));
        }

        return map;
    }

    /// <summary>
    /// NLog magic to get the file name associated with a logging target name.  Dig through any
    /// wrapper filters until we get to a file target and return the directory name.
    /// </summary>
    /// <param name="targetName">The target name.</param>
    /// <returns>The path to the NLog directory.</returns>
    private static string GetNLogLogDirectoryPath(string targetName)
    {
        Target target = NLog.LogManager.Configuration.FindTargetByName(targetName);
        if (target == null)
            throw new ArgumentException("target(" + targetName + ") not found");
        while (target is WrapperTargetBase)
        {
            target = (target as WrapperTargetBase).WrappedTarget;
        }
        FileTarget fileTarget = target as FileTarget;
        if (fileTarget == null)
            throw new ArgumentException("could not find file target for target(" + targetName + ")");
        // NLog does not give up the path of the target easily...
        LogEventInfo logEventInfo = new LogEventInfo { TimeStamp = DateTime.Now };
        string fileName = fileTarget.FileName.Render(logEventInfo);
        string directoryName = Path.GetDirectoryName(fileName) ?? ".";
        return directoryName;
    }

    /// <summary>
    /// Simple store client used to submit a store job to the listening store SCP and
    /// listen for the status of the job.
    /// </summary>
    private class StoreSCU : IStoreClientListener
    {
        private static readonly ILogger SCULogger = LogManager.GetCurrentClassLogger();
        private readonly StoreClient _client = new StoreClient();

        /// <summary>
        /// Submit the given store job using this classes store client.
        /// </summary>
        /// <param name="job">The job to submit.</param>
        public void SubmitStoreJob(StoreJobDescription job)
        {
            _client.SubmitStoreJob(job, this);
        }

        /// <summary>
        /// Implementation of StoreClientListener interface.
        /// </summary>
        public void StoreObjectComplete(StoreJobInstanceStatus status)
        {
            int base10DimseStatus = status.DimseStatusCode;
            string hexDimseStatus = base10DimseStatus.ToString("X");
            string stringDimseStatus = StoreDimseStatus.GetStatusAsString(status.DimseStatusCode);
            SCULogger.InfoFormat("Received storeObjectComplete event status({0}) (0x{1}), {2}", base10DimseStatus, hexDimseStatus, stringDimseStatus);
        }

        /// <summary>
        /// Implementation of StoreClientListener interface.
        /// </summary>
        public void StoreJobComplete(StoreJobStatus status)
        {
            SCULogger.InfoFormat("Received storeJobComplete event {0}status = {1}{0}statusInfo = {2}",
                Environment.NewLine,
                JobStatus.GetStatusAsString(status.Status),
                JobStatusInfo.GetStatusAsString(status.StatusInfo));
        }
    }

    /// <summary>
    /// Simple callback store server used to handle association and CStore requests.
    /// </summary>
    private class CallbackStoreServer : AssociationListenerAdapter, IAssociationConfigPolicyManager
    {
        private static readonly ILogger StoreServerLogger = LogManager.GetCurrentClassLogger();

        private readonly List<LogDebugFlags> debugFlagList = new List<LogDebugFlags>
        {
            DF.None,
            DF.Connection,
            DF.Connection | DF.DimseReadSummary | DF.DimseWriteSummary,
            DF.Connection | DF.DimseWire | DF.AcsePdu | DF.DataPdu,
            DF.All, 
        };

        private int _debugFlagIndex /*= 0*/;

        /// <summary>
        /// Returns a DicomSessionSettings object to be used for the association.
        /// </summary>
        /// <param name="assoc">The AssociationAcceptor for the given association</param>
        /// <returns>A default DicomSessionSettings object</returns>
        public DicomSessionSettings GetSessionSettings(AssociationAcceptor assoc)
        {
            DicomSessionSettings ss = new DicomSessionSettings();
            ss.IsDebugEnabled = true;
            ss.DebugFlags = debugFlagList[_debugFlagIndex++];
            _debugFlagIndex %= debugFlagList.Count;
            // Before each log message, output the connection id. See the SessionLogging's app config for how to 
            // configure the layout to output the log context for a connection id.
            ss.LogContext.Add("ConnectionId", assoc.ConnectionID);
            return ss;
        }

        /// <summary>
        /// Handle the association that caused this 'beginAssociation' method to be called.
        /// </summary>
        /// <param name="assoc">The AssociationAcceptor for the given association</param>
        public override void BeginAssociation(AssociationAcceptor assoc)
        {
            StoreServerLogger.InfoFormat(assoc.SessionSettings, "beginAssociation: {0}", assoc.AssociationInfo);
            assoc.RegisterServiceClassProvider(new StoreSCP(assoc, null, CStore));
        }

        /// <summary>
        /// Handle the association that caused this 'endAssociation' method to be called.
        /// </summary>
        /// <param name="assoc">The AssociationAcceptor for the given association</param>
        public override void EndAssociation(AssociationAcceptor assoc)
        {
            StoreServerLogger.InfoFormat(assoc.SessionSettings, "endAssociation: {0}", assoc.AssociationInfo);

            // Add the session id for this association to the list of seen session ids if not already
            string sessionId = assoc.SessionSettings.SessionId.ToString();
            if (!SessionIds.Contains(sessionId))
                SessionIds.Add(sessionId);
        }

        /// <summary>
        /// Writes some information from the incoming dataset to the console.
        /// </summary>
        /// <param name="acceptor">The AssociationAcceptor for the given association</param>
        /// <param name="request">The inbound CStoreRequest</param>
        /// <returns>A successful CStoreResponse</returns>
        private CStoreResponse CStore(AssociationAcceptor acceptor, CStoreRequest request)
        {
            CStoreResponse response = new CStoreResponse(request);
            StoreServerLogger.InfoFormat(acceptor.SessionSettings, "CallbackStoreServer: patient's name is {0} from {1} to {2}",
                request.Data.GetElementStringValue(Tags.PatientName), acceptor.AssociationInfo.CallingTitle, acceptor.AssociationInfo.CalledPresentationAddress);
            request.Data.ExpandStreamingModeData(false); // we don't need the pixel data for this example

            return response;
        }
    }
}