LAUREL BRIDGE

LaurelBridge.DCFExamples.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.
Remarks

Supported OS Platforms:

  • Windows - .Net Framework 4.7.2 64-bit and 32-bit
  • Windows - .Net Core 2.1 64-bit and 32-bit
  • Linux - .Net Core 2.1 64-bit

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
            {
                // For .NET Core applications, the build system apparently copies the App.config file to <AssemblyNameWithoutExtension>.dll.config instead of the
                // <AssemblyNameWithoutExtension>.exe.config. Also, the NLog 4.5.11 on .NET Core does not load configuration automatically from this .dll.config file.
                // Therefore, check below if we are building a .NET Core application or not. Even for non .NET Core applications, specify the path to the app config
                // file explicitly to make it clear which file is being used instead of relying on NLog default behavior.
                string executingAssemblyName = System.Reflection.Assembly.GetExecutingAssembly().Location;
#if NETCOREAPP
                string nLogConfigFileName = Path.ChangeExtension(executingAssemblyName, ".dll.config");
#else
                string nLogConfigFileName = Path.ChangeExtension(executingAssemblyName, ".exe.config");
#endif
                LogManager.LogAdapter = new NLogLogAdapter(nLogConfigFileName);

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

                mgr = StartSCP(10104);
                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 = NLogLogAdapter.GetNLogLogDirectoryPath("SessionFileLogger");

                // Prompts the user to view selected log file contents
                ViewLogFiles(logDir);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception caught during execution: {0}", e);
                Environment.ExitCode = 1;
            }
            finally
            {
                // 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 = "10104";
            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 using the pager. 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]);
                    Pager.View(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>
        /// 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;
            }
        }
    }