├── 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 | [](./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 | 
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 | 
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 |
--------------------------------------------------------------------------------