├── src ├── event32.png ├── command32.png ├── endpoint32.png ├── InternalsVisibleTo.cs ├── packages.config ├── App.config ├── ServiceControlModel.cs ├── AllProcessedMessagesRoutedMessageSource.cs ├── Properties │ └── AssemblyInfo.cs ├── RoutingVisualization.sln ├── Model.cs ├── ModelBuilder.cs ├── Program.cs ├── RoutingVisualization.csproj ├── DgmlRouteDocFactory.cs └── NodeStrategy.cs ├── how-does-it-work.PNG ├── running-screenshot.PNG ├── sample ├── video-store-route-graph.PNG └── video-store-route-graph.zip ├── RoutingVisualization.UnitTests ├── packages.config ├── EndpointNodeStrategyTests.cs ├── Properties │ └── AssemblyInfo.cs ├── ModelBuilderTests.cs └── RoutingVisualization.UnitTests.csproj ├── LICENSE ├── .gitignore └── README.md /src/event32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParticularLabs/RoutingVisualization/HEAD/src/event32.png -------------------------------------------------------------------------------- /src/command32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParticularLabs/RoutingVisualization/HEAD/src/command32.png -------------------------------------------------------------------------------- /src/endpoint32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParticularLabs/RoutingVisualization/HEAD/src/endpoint32.png -------------------------------------------------------------------------------- /how-does-it-work.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParticularLabs/RoutingVisualization/HEAD/how-does-it-work.PNG -------------------------------------------------------------------------------- /running-screenshot.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParticularLabs/RoutingVisualization/HEAD/running-screenshot.PNG -------------------------------------------------------------------------------- /src/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("RoutingVisualization.UnitTests")] -------------------------------------------------------------------------------- /sample/video-store-route-graph.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParticularLabs/RoutingVisualization/HEAD/sample/video-store-route-graph.PNG -------------------------------------------------------------------------------- /sample/video-store-route-graph.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParticularLabs/RoutingVisualization/HEAD/sample/video-store-route-graph.zip -------------------------------------------------------------------------------- /src/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /RoutingVisualization.UnitTests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RoutingVisualization.UnitTests/EndpointNodeStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace RoutingVisualization.UnitTests 4 | { 5 | public class LogicalRoutingNodeStrategyTests 6 | { 7 | [Test] 8 | public void NodeIdForANullEndpointShouldBeNull() 9 | { 10 | var logicalRoutingNodeStrategy = new LogicalRoutingNodeStrategy(); 11 | 12 | var nodeId = logicalRoutingNodeStrategy.GetNodeId(null); 13 | 14 | Assert.IsNull(nodeId); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/ServiceControlModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace RoutingVisualization 4 | { 5 | public class ProcessedMessage 6 | { 7 | public MessageMetadata MessageMetadata { get; set; } 8 | public Dictionary Headers { get; set; } 9 | } 10 | 11 | public class EndpointDetails 12 | { 13 | public string Name { get; set; } 14 | public string Host { get; set; } 15 | } 16 | 17 | public class MessageMetadata 18 | { 19 | public string MessageType { get; set; } 20 | public EndpointDetails SendingEndpoint { get; set; } 21 | public EndpointDetails ReceivingEndpoint { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/AllProcessedMessagesRoutedMessageSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Raven.Client; 3 | 4 | namespace RoutingVisualization 5 | { 6 | class AllProcessedMessagesRoutedMessageSource 7 | { 8 | private readonly IDocumentStore _store; 9 | 10 | public AllProcessedMessagesRoutedMessageSource(IDocumentStore store) 11 | { 12 | _store = store; 13 | } 14 | 15 | public void RegisterListener(Action onNext) 16 | { 17 | using (var session = _store.OpenSession()) 18 | using (var stream = session.Advanced.Stream("ProcessedMessage")) 19 | { 20 | var count = 0; 21 | while (stream.MoveNext()) 22 | { 23 | Console.Write($"\rMessage #{count++}"); 24 | onNext(stream.Current.Document); 25 | } 26 | Console.WriteLine(); 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Particular Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RoutingVisualization")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("RoutingVisualization")] 13 | [assembly: AssemblyCopyright("Copyright © Particular Labs 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("19b59d01-ace4-40c0-91fd-2fdefc2c3252")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /RoutingVisualization.UnitTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RoutingVisualization.UnitTests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("RoutingVisualization.UnitTests")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("5d655ef8-a399-4e13-b0c4-3bd31ad94fe3")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/RoutingVisualization.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.23107.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoutingVisualization", "RoutingVisualization.csproj", "{19B59D01-ACE4-40C0-91FD-2FDEFC2C3252}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoutingVisualization.UnitTests", "..\RoutingVisualization.UnitTests\RoutingVisualization.UnitTests.csproj", "{5D655EF8-A399-4E13-B0C4-3BD31AD94FE3}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {19B59D01-ACE4-40C0-91FD-2FDEFC2C3252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {19B59D01-ACE4-40C0-91FD-2FDEFC2C3252}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {19B59D01-ACE4-40C0-91FD-2FDEFC2C3252}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {19B59D01-ACE4-40C0-91FD-2FDEFC2C3252}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {5D655EF8-A399-4E13-B0C4-3BD31AD94FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5D655EF8-A399-4E13-B0C4-3BD31AD94FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5D655EF8-A399-4E13-B0C4-3BD31AD94FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {5D655EF8-A399-4E13-B0C4-3BD31AD94FE3}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /RoutingVisualization.UnitTests/ModelBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | 4 | namespace RoutingVisualization.UnitTests 5 | { 6 | public class ModelBuilderTests 7 | { 8 | [Test] 9 | public void CanAcceptMessageWithoutHeaders() 10 | { 11 | var endpointNodeStrategy = new LogicalRoutingNodeStrategy(); 12 | var messageNodeStrategy = new CollapseMessagesToSameReceiverMessageNodeStrategy(endpointNodeStrategy); 13 | var nodeStrategy = new NodeStrategy(endpointNodeStrategy, messageNodeStrategy); 14 | var modelBuilder = new ModelBuilder(nodeStrategy); 15 | 16 | var message = new ProcessedMessage 17 | { 18 | Headers = new Dictionary() 19 | }; 20 | 21 | Assert.DoesNotThrow( 22 | () => modelBuilder.Accept(message) 23 | ); 24 | } 25 | 26 | [Test] 27 | public void CanAcceptMessageWithoutMessageType() 28 | { 29 | var endpointNodeStrategy = new LogicalRoutingNodeStrategy(); 30 | var messageNodeStrategy = new CollapseMessagesToSameReceiverMessageNodeStrategy(endpointNodeStrategy); 31 | var nodeStrategy = new NodeStrategy(endpointNodeStrategy, messageNodeStrategy); 32 | var modelBuilder = new ModelBuilder(nodeStrategy); 33 | 34 | var message = new ProcessedMessage 35 | { 36 | Headers = new Dictionary 37 | { 38 | ["NServiceBus.Intent"] = "Send" 39 | } 40 | }; 41 | 42 | Assert.DoesNotThrow( 43 | () => modelBuilder.Accept(message) 44 | ); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Model.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace RoutingVisualization 7 | { 8 | public class Model 9 | { 10 | private readonly ConcurrentDictionary> _endpoints = new ConcurrentDictionary>(); 11 | private readonly ConcurrentDictionary> _messages = new ConcurrentDictionary>(); 12 | private readonly ConcurrentDictionary, IDictionary> _links = new ConcurrentDictionary, IDictionary>(); 13 | 14 | public IDictionary GetEndpoint(string endpointId) 15 | { 16 | return _endpoints.GetOrAdd(endpointId, id => new Dictionary 17 | { 18 | ["Id"] = id, 19 | ["Category"] = "Endpoint" 20 | }); 21 | } 22 | 23 | public IDictionary GetMessage(string messageId) 24 | { 25 | return _messages.GetOrAdd(messageId, id => new Dictionary 26 | { 27 | ["Id"] = id, 28 | ["Category"] = "Message" 29 | }); 30 | } 31 | 32 | public IDictionary GetLink(string source, string target) 33 | { 34 | return _links.GetOrAdd(Tuple.Create(source, target), id => new Dictionary 35 | { 36 | ["Source"] = source, 37 | ["Target"] = target 38 | }); 39 | } 40 | 41 | public IEnumerable> GetNodes() 42 | { 43 | return _endpoints.Values.Concat(_messages.Values); 44 | } 45 | 46 | public IEnumerable> GetLinks() 47 | { 48 | return _links.Values; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/ModelBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace RoutingVisualization 4 | { 5 | public class ModelBuilder 6 | { 7 | private readonly Model _model; 8 | private readonly NodeStrategy _nodeStrategy; 9 | 10 | public ModelBuilder(NodeStrategy nodeStrategy) 11 | { 12 | _model = new Model(); 13 | _nodeStrategy = nodeStrategy; 14 | } 15 | 16 | public void Accept(ProcessedMessage message) 17 | { 18 | if (message?.Headers == null) 19 | { 20 | return; 21 | } 22 | 23 | string intent; 24 | if (!message.Headers.TryGetValue("NServiceBus.MessageIntent", out intent)) 25 | { 26 | return; 27 | } 28 | if (intent == "Subscribe" || intent == "Unsubscribe") 29 | return; 30 | 31 | var senderId = _nodeStrategy.GetNodeId(message.MessageMetadata.SendingEndpoint); 32 | if (senderId != null) 33 | { 34 | var senderNode = _model.GetEndpoint(senderId); 35 | senderNode["Label"] = message.MessageMetadata.SendingEndpoint.Name; 36 | } 37 | 38 | var receiverId = _nodeStrategy.GetNodeId(message.MessageMetadata.ReceivingEndpoint); 39 | if(receiverId != null) 40 | { 41 | var receiverNode = _model.GetEndpoint(receiverId); 42 | receiverNode["Label"] = message.MessageMetadata.ReceivingEndpoint.Name; 43 | } 44 | 45 | var messageId = _nodeStrategy.GetNodeId(message); 46 | if(messageId != null) 47 | { 48 | var messageNode = _model.GetMessage(messageId); 49 | messageNode["Intent"] = intent; 50 | if (!string.IsNullOrWhiteSpace(message.MessageMetadata.MessageType)) 51 | { 52 | messageNode["Label"] = message.MessageMetadata.MessageType.Split('.').Last(); 53 | } 54 | } 55 | 56 | if (senderId != null && messageId != null) 57 | { 58 | var senderLink = _model.GetLink(senderId, messageId); 59 | senderLink["Intent"] = intent; 60 | } 61 | 62 | if (messageId != null && receiverId != null) 63 | { 64 | var receiverLink = _model.GetLink(messageId, receiverId); 65 | receiverLink["Intent"] = intent; 66 | } 67 | } 68 | 69 | public Model GetModel() 70 | { 71 | return _model; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /RoutingVisualization.UnitTests/RoutingVisualization.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {5D655EF8-A399-4E13-B0C4-3BD31AD94FE3} 8 | Library 9 | Properties 10 | RoutingVisualization.UnitTests 11 | RoutingVisualization.UnitTests 12 | v4.5.2 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\src\packages\NUnit.2.6.4\lib\nunit.framework.dll 35 | True 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {19b59d01-ace4-40c0-91fd-2fdefc2c3252} 54 | RoutingVisualization 55 | 56 | 57 | 58 | 59 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Raven.Client; 6 | using Raven.Client.Document; 7 | using Raven.Imports.Newtonsoft.Json; 8 | 9 | namespace RoutingVisualization 10 | { 11 | static class Program 12 | { 13 | static void Main(string[] args) 14 | { 15 | var serviceControlDataUrl = ConfigurationManager.AppSettings["ServiceControl/RavenAddress"]; 16 | var store = GetDocumentStore(serviceControlDataUrl); 17 | 18 | var modelBuilder = new ModelBuilder(GetNodeStrategy()); 19 | 20 | var dataSource = new AllProcessedMessagesRoutedMessageSource(store); 21 | 22 | dataSource.RegisterListener(modelBuilder.Accept); 23 | var model = modelBuilder.GetModel(); 24 | Console.WriteLine("Model generated"); 25 | 26 | var dgml = DgmlRouteDocFactory.CreateDgml(model); 27 | var outputFileName = args.FirstOrDefault() ?? "route-graph"; 28 | if (!outputFileName.EndsWith(".dgml", StringComparison.CurrentCultureIgnoreCase)) 29 | { 30 | outputFileName += ".dgml"; 31 | } 32 | 33 | dgml.Save(outputFileName); 34 | 35 | Console.WriteLine($"Created {outputFileName}"); 36 | } 37 | 38 | private static IDocumentStore GetDocumentStore(string url) 39 | { 40 | var store = new DocumentStore 41 | { 42 | Url = url, 43 | Conventions = 44 | { 45 | // Prevents $type from interfering with deserialization of EndpointDetails 46 | CustomizeJsonSerializer = serializer => serializer.TypeNameHandling = TypeNameHandling.None 47 | }, 48 | }; 49 | 50 | store.Initialize(); 51 | 52 | try 53 | { 54 | store.DatabaseCommands.GetStatistics(); 55 | } 56 | catch 57 | { 58 | Console.WriteLine($"Unable to connect to the configured ServiceControl database.\nPlease check that opening {url} in the browser shows the RavenDB Management Studio."); 59 | Console.WriteLine($"You can change this location by adjusting the value of ServiceControl/RavenAddress in the config file:\n\t{AppDomain.CurrentDomain.SetupInformation.ConfigurationFile}"); 60 | throw; 61 | } 62 | 63 | Console.WriteLine($"Reading messages from {store.Url}"); 64 | 65 | return store; 66 | } 67 | 68 | private static NodeStrategy GetNodeStrategy() 69 | { 70 | // Logical Routing Strategy collapses endpoints with the same name into a single endpoint. Even if running on different hosts 71 | var logicalEndpoints = new LogicalRoutingNodeStrategy(); 72 | // Physical Routing Strategy will split up endpoints that have the same name but run on different hosts 73 | //var physicalEndpoints = new PhysicalRoutingNodeStrategy(); 74 | 75 | var endpoints = logicalEndpoints; 76 | 77 | // All messages of the same type that come from the same sender will be collapsed into a single message node 78 | var collapseFromSender = new CollaseMessagesFromSameSenderMessageNodeStrategy(endpoints); 79 | 80 | // All messages of the same type that go to the same receiver will be collapsed into a single message node 81 | //var collapseToReceiver = new CollapseMessagesToSameReceiverMessageNodeStrategy(endpoints); 82 | 83 | // All published events of the same type and sender will collapse into one, 84 | // all non-events of the same type and sent to the same reciever collapses into one 85 | //var intentBasedMessageNodeStrategy = new IntentBasedMessageNodeStrategy(collapseToReceiver); 86 | //intentBasedMessageNodeStrategy.Add("Publish", collapseFromSender); 87 | 88 | var messages = collapseFromSender; 89 | 90 | return new NodeStrategy(endpoints, messages); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/RoutingVisualization.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {19B59D01-ACE4-40C0-91FD-2FDEFC2C3252} 8 | Exe 9 | Properties 10 | RoutingVisualization 11 | RoutingVisualization 12 | v4.5.2 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | packages\RavenDB.Client.3.5.9\lib\net45\Raven.Abstractions.dll 38 | 39 | 40 | packages\RavenDB.Client.3.5.9\lib\net45\Raven.Client.Lightweight.dll 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Always 71 | 72 | 73 | Always 74 | 75 | 76 | Always 77 | 78 | 79 | 80 | 87 | -------------------------------------------------------------------------------- /src/DgmlRouteDocFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Xml.Linq; 4 | 5 | namespace RoutingVisualization 6 | { 7 | public static class DgmlRouteDocFactory 8 | { 9 | private static readonly XNamespace DgmlNamespace = XNamespace.Get("http://schemas.microsoft.com/vs/2009/dgml"); 10 | 11 | public static XDocument CreateDgml(Model model) 12 | { 13 | return new XDocument( 14 | new XElement(DgmlNamespace + "DirectedGraph", 15 | new XAttribute("GraphDirection", "LeftToRight"), 16 | new XAttribute("Layout", "Sugiyama"), 17 | new XElement(DgmlNamespace + "Nodes", Nodes(model.GetNodes())), 18 | new XElement(DgmlNamespace + "Links", Links(model.GetLinks())), 19 | new XElement(DgmlNamespace + "Categories", GetCategories()), 20 | new XElement(DgmlNamespace + "Properties", GetProperties()), 21 | new XElement(DgmlNamespace + "Styles", GetStyles()) 22 | ) 23 | ); 24 | } 25 | 26 | private static IEnumerable Nodes(IEnumerable> data) 27 | { 28 | return from node in data 29 | select new XElement(DgmlNamespace + "Node", 30 | from attr in node select new XAttribute(attr.Key, attr.Value) 31 | ); 32 | } 33 | 34 | private static IEnumerable Links(IEnumerable> links) 35 | { 36 | return from link in links 37 | select new XElement(DgmlNamespace + "Link", 38 | from attr in link select new XAttribute(attr.Key, attr.Value) 39 | ); 40 | } 41 | 42 | private static IEnumerable GetCategories() 43 | { 44 | yield break; 45 | } 46 | 47 | private static IEnumerable GetProperties() 48 | { 49 | yield break; 50 | } 51 | 52 | private static IEnumerable GetStyles() 53 | { 54 | yield return MakeStyle("Node", "MessageType", "Publish", 55 | "Intent = 'Publish'", 56 | Set("Icon", @".\event32.png") 57 | ); 58 | 59 | yield return MakeStyle("Node", "MessageType", "Send", 60 | "Intent = 'Send'", 61 | Set("Icon", @".\command32.png") 62 | ); 63 | 64 | yield return MakeStyle("Node", "MessageType", "Reply", 65 | "Intent = 'Reply'", 66 | Set("Icon", @".\command32.png") 67 | ); 68 | 69 | yield return MakeStyle("Node", "Type", "Endpoint", 70 | "HasCategory('Endpoint')", 71 | Set("Icon", @".\endpoint32.png"), 72 | Set("Background", "#FF8998BC") 73 | ); 74 | 75 | yield return MakeStyle("Node", "Type", "Message", 76 | "HasCategory('Message')" 77 | ); 78 | 79 | yield return MakeStyle("Link", "Intent", "Publish", 80 | "Intent = 'Publish'", 81 | Set("StrokeDashArray", "5") 82 | ); 83 | 84 | yield return MakeStyle("Link", "Intent", "Reply", 85 | "Intent = 'Reply'", 86 | Set("StrokeDashArray", "1,5") 87 | ); 88 | 89 | yield return MakeStyle("Link", "Intent", "Send", 90 | "Intent = 'Send'" 91 | ); 92 | } 93 | 94 | private static XElement MakeStyle(string targetType, string groupLabel, string valueLabel, 95 | string conditionExpression, params XElement[] sets) 96 | { 97 | return new XElement(DgmlNamespace + "Style", new XAttribute("TargetType", targetType), new XAttribute("GroupLabel", groupLabel), new XAttribute("ValueLabel", valueLabel), 98 | new[] { new XElement(DgmlNamespace + "Condition", new XAttribute("Expression", conditionExpression)) } 99 | .Concat(sets) 100 | ); 101 | } 102 | 103 | private static XElement Set(string property, string value) 104 | { 105 | return new XElement(DgmlNamespace + "Setter", 106 | new XAttribute("Property", property), 107 | new XAttribute("Value", value) 108 | ); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/NodeStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace RoutingVisualization 6 | { 7 | public class NodeStrategy : INodeStrategy, INodeStrategy 8 | { 9 | private INodeStrategy _endpointNodeStrategy; 10 | private INodeStrategy _messageNodeStrategy; 11 | 12 | public NodeStrategy(INodeStrategy endpointNodeStrategy, INodeStrategy messageNodeStrategy) 13 | { 14 | _endpointNodeStrategy = endpointNodeStrategy; 15 | _messageNodeStrategy = messageNodeStrategy; 16 | } 17 | 18 | public string GetNodeId(EndpointDetails details) 19 | { 20 | return _endpointNodeStrategy.GetNodeId(details); 21 | } 22 | 23 | public string GetNodeId(ProcessedMessage details) 24 | { 25 | return _messageNodeStrategy.GetNodeId(details); 26 | } 27 | } 28 | 29 | public interface INodeStrategy 30 | { 31 | string GetNodeId(T details); 32 | } 33 | 34 | public abstract class NodeStrategy : INodeStrategy 35 | { 36 | public abstract string GetNodeId(T details); 37 | 38 | protected static string ToNodeName(params string[] parts) 39 | { 40 | return Regex.Replace(string.Join("_", parts), "[-.]", "_").ToLower(); 41 | } 42 | } 43 | 44 | 45 | class PhysicalRoutingNodeStrategy : NodeStrategy 46 | { 47 | public override string GetNodeId(EndpointDetails details) 48 | { 49 | return ToNodeName(details.Host, details.Name); 50 | } 51 | } 52 | 53 | class LogicalRoutingNodeStrategy : NodeStrategy 54 | { 55 | public override string GetNodeId(EndpointDetails details) 56 | { 57 | if (details == null) 58 | return null; 59 | return ToNodeName(details.Name); 60 | } 61 | } 62 | 63 | class CollaseMessagesFromSameSenderMessageNodeStrategy : NodeStrategy 64 | { 65 | private NodeStrategy _endpointNodeStrategy; 66 | 67 | public CollaseMessagesFromSameSenderMessageNodeStrategy(NodeStrategy endpointNodeStrategy) 68 | { 69 | _endpointNodeStrategy = endpointNodeStrategy; 70 | } 71 | 72 | public override string GetNodeId(ProcessedMessage details) 73 | { 74 | var intent = details.Headers["NServiceBus.MessageIntent"]; 75 | var sendingEndpointNodeId = _endpointNodeStrategy.GetNodeId(details.MessageMetadata.SendingEndpoint); 76 | return ToNodeName(sendingEndpointNodeId, intent, details.MessageMetadata.MessageType); 77 | } 78 | } 79 | 80 | class CollapseMessagesToSameReceiverMessageNodeStrategy : NodeStrategy 81 | { 82 | private NodeStrategy _endpointNodeStrategy; 83 | 84 | public CollapseMessagesToSameReceiverMessageNodeStrategy(NodeStrategy endpointNodeStrategy) 85 | { 86 | _endpointNodeStrategy = endpointNodeStrategy; 87 | } 88 | 89 | public override string GetNodeId(ProcessedMessage details) 90 | { 91 | var intent = details.Headers["NServiceBus.MessageIntent"]; 92 | var receivingEndpointNodeId = _endpointNodeStrategy.GetNodeId(details.MessageMetadata.ReceivingEndpoint); 93 | return ToNodeName(receivingEndpointNodeId, intent, details.MessageMetadata.MessageType); 94 | } 95 | } 96 | 97 | class IntentBasedMessageNodeStrategy : INodeStrategy 98 | { 99 | private IDictionary> _strategyMap = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); 100 | private INodeStrategy _defaultStrategy; 101 | 102 | public IntentBasedMessageNodeStrategy(INodeStrategy defaultStrategy) 103 | { 104 | _defaultStrategy = defaultStrategy; 105 | } 106 | 107 | public string GetNodeId(ProcessedMessage details) 108 | { 109 | var intent = details.Headers["NServiceBus.MessageIntent"]; 110 | 111 | INodeStrategy strategy; 112 | if (!_strategyMap.TryGetValue(intent, out strategy)) 113 | strategy = _defaultStrategy; 114 | 115 | return strategy.GetNodeId(details); 116 | } 117 | 118 | public void Add(string intent, INodeStrategy strategy) 119 | { 120 | _strategyMap.Add(intent, strategy); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Routing Visualization 2 | 3 | A command line tool that uses audit data stored in ServiceControl to construct a visualization of endpoints and the messages being routed between them. 4 | 5 | ## Why did we create this? 6 | 7 | One of the key benefits of a solution built on top of NServiceBus is that endpoints can be developed, deployed and continue to evolve independently from one another. This can lead to a situation where all of the communication happening between all of the endpoints is not well known or documented. The ability to visualize an existing system built on NServiceBus is a common request we hear from our users. 8 | 9 | This experiment demonstrates one method we could use to fulfill this request. 10 | 11 | This is an example of the type of visualization we are able to create with this tool. Click the image to download a copy of the diagram that you can open and manipulate in Visual Studio. 12 | 13 | [![Showcase sample rout graph](./sample/video-store-route-graph.PNG)](./sample/video-store-route-graph.zip) 14 | 15 | ## What does it do? 16 | 17 | When [NServiceBus auditing](http://docs.particular.net/nservicebus/operations/auditing) is enabled, each message that is processed by an endpoint is sent to an audit queue along with some metadata about how it was processed. [ServiceControl](http://docs.particular.net/platform/#servicecontrol-the-foundation) will read all of the messages from the audit queue and store them in an embedded database. 18 | 19 | This tool runs through all of the audited messages in the ServiceControl database and uses the processing metadata to construct a visualization of your system that you can open in Visual Studio. The generated graph contains nodes for each endpoint and message type and draws edges between them to show the flow of messages. 20 | 21 | ![Data flow for Routing Visualization tool](./how-does-it-work.PNG) 22 | 23 | ## How do I use it? 24 | 25 | 1. [Enable Auditing](http://docs.particular.net/nservicebus/operations/auditing) on each of your endpoints and configure each one to send audited messages to a central audit queue 26 | 2. [Install ServiceControl](http://docs.particular.net/servicecontrol/installation) and configure it to read audit messages from the central audit queue 27 | 3. Configure the ServiceControl Audit instance to [expose it's embedded database](https://docs.particular.net/servicecontrol/audit-instances/maintenance-mode) 28 | 4. Get the latest copy of [this tool](https://github.com/ParticularLabs/RoutingVisualization) 29 | 5. Update `RoutingVisualization.exe.config` to point to the location of your Maintenance Port of you ServiceControl Audit instance. The default is `http://localhost:44445/`. You can test this url by opening it in a browser. It should show the RavenDB Management Studio 30 | 6. Open a command prompt window and run the tool: `RoutingVisualization.exe `. If you do not specificy a file name then `route-graph.dgml` will be used. 31 | 32 | ![Screenshot of the tool running](./running-screenshot.PNG) 33 | 34 | The tool will connect to the ServiceControl embedded database and iterate through every audited message building up an internal model as it goes. Once it has read all of the messages the model is serialized as [DGML](https://en.wikipedia.org/wiki/DGML). The DGML file is XML that specifies all of the endpoints, message types and the links between them. You can open a DGML file in Visual Studio. 35 | 36 | The latest versions of Visual Studio do not include a DGML viewer by default. You can install it from Visual Studio by selecting _Tools > Get tools and features... > Individual Component > Code tools > DGML Editor_. 37 | 38 | ## Notes 39 | 40 | * This tool is a prototype and is not recommended for use in a production environment. 41 | * Using the data in ServiceControl is great for showing the messages that have been sent/published and successfully processed. There is some downsides to using this data to generate a visualization of this type. Here are some examples of issues that the generated diagram might have: 42 | * Messages missing from the diagram because they've never been sent - some messages are only sent under very specific circumstances. If those circumstances have not come up and the message has not been sent then there will be no record of it in ServiceControl and those messages will not appear on the diagram. 43 | * Events missing from the diagram because they had no subscribers - the data in ServiceControl comes from successfully processed messages in the audit queue. If a message is published but had no subscribers then it will not end up in the audit queue and will not appear on the diagram. 44 | * Messages missing from the diagram because they haven't been sent recently - ServiceControl does not keep audited data forever. After a period of time, audit data expires and is deleted from the database. If a message is sent infrequently then it is possible that there will be no record of it in ServiceControl when the tool runs. You can [configure automatic expiration of ServiceControl data](http://docs.particular.net/servicecontrol/how-purge-expired-data) to increase the window of time that audit data is kept. 45 | * Messages missing from the diagram because they failed processing - currently this tool only reads data that comes from the audit queue. If processing fails on a message it will go the error queue instead of the audit queue. ServiceControl can be configure to read these messages as well but they are not used in the generation of the diagram. 46 | * Old endpoints and messages will persist until the data they were derived from expires - If an endpoint or a message type is decommissioned it will continue to appear in the diagram until the data in ServiceControl expires. Similarly, if an endpoint or message type is renamed, the old name will continue to appear on the diagram until the audit data in ServiceControl containing the old name expires. 47 | 48 | ## Let us know what you think 49 | 50 | We'd really like to hear your thoughts on this. Feel free to [open an issue](https://github.com/ParticularLabs/RoutingVisualization/issues/new) and tell us what you like and what needs improvement. 51 | --------------------------------------------------------------------------------