@@ -69,6 +69,7 @@ private sealed class StringListPool(int initialCapacity)
6969 private readonly string versionInformation ;
7070 private readonly ArrayBufferWriterPool bufferWriterPool = new ( initialCapacity : 256 ) ;
7171 private readonly StringListPool responseLineListPool = new ( initialCapacity : 32 ) ;
72+ private readonly Dictionary < string , IPlugin > plugins = new ( StringComparer . Ordinal ) ;
7273
7374 /// <summary>
7475 /// Gets a value indicating whether the <c>munin master</c> supports
@@ -80,6 +81,21 @@ private sealed class StringListPool(int initialCapacity)
8081 /// </seealso>
8182 protected bool IsDirtyConfigEnabled { get ; private set ; }
8283
84+ /// <summary>
85+ /// Gets a value indicating whether the <c>munin master</c> supports
86+ /// <c>multigraph</c> protocol extension and enables it.
87+ /// </summary>
88+ /// <seealso cref="HandleCapCommandAsync"/>
89+ /// <seealso href="https://guide.munin-monitoring.org/en/latest/plugin/protocol-multigraph.html">
90+ /// Protocol extension: multiple graphs from one plugin
91+ /// </seealso>
92+ /// <seealso href="https://guide.munin-monitoring.org/en/latest/plugin/multigraphing.html">
93+ /// Multigraph plugins
94+ /// </seealso>
95+ protected bool IsMultigraphEnabled { get ; private set ; }
96+
97+ private string joinedPluginList = string . Empty ;
98+
8399 public MuninProtocolHandler (
84100 IMuninNodeProfile profile
85101 )
@@ -88,6 +104,24 @@ IMuninNodeProfile profile
88104
89105 banner = $ "# munin node at { profile . HostName } ";
90106 versionInformation = $ "munins node on { profile . HostName } version: { profile . Version } ";
107+
108+ ReinitializePluginDictionary ( ) ;
109+ }
110+
111+ private void ReinitializePluginDictionary ( )
112+ {
113+ var flattenMultigraphPlugins = ! IsMultigraphEnabled ;
114+
115+ plugins . Clear ( ) ;
116+
117+ foreach ( var plugin in profile . PluginProvider . EnumeratePlugins ( flattenMultigraphPlugins ) ) {
118+ plugins [ plugin . Name ] = plugin ; // duplicate plugin names are not considered
119+ }
120+
121+ joinedPluginList = string . Join (
122+ ' ' ,
123+ profile . PluginProvider . EnumeratePlugins ( flattenMultigraphPlugins ) . Select ( static plugin => plugin . Name )
124+ ) ;
91125 }
92126
93127 /// <inheritdoc cref="IMuninProtocolHandler.HandleTransactionStartAsync"/>
@@ -397,29 +431,52 @@ CancellationToken cancellationToken
397431 /// even when <c>dirtyconfig</c> is enabled.
398432 /// </remarks>
399433 /// <seealso cref="IsDirtyConfigEnabled"/>
434+ /// <seealso cref="IsMultigraphEnabled"/>
400435 /// <seealso href="https://guide.munin-monitoring.org/en/latest/master/network-protocol.html">
401436 /// Data exchange between master and node - `cap` command
402437 /// </seealso>
403438 /// <seealso href="https://guide.munin-monitoring.org/en/latest/plugin/protocol-dirtyconfig.html">
404439 /// Protocol extension: dirtyconfig
405440 /// </seealso>
441+ /// <seealso href="https://guide.munin-monitoring.org/en/latest/plugin/protocol-multigraph.html">
442+ /// Protocol extension: multiple graphs from one plugin
443+ /// </seealso>
406444 protected virtual ValueTask HandleCapCommandAsync (
407445 IMuninNodeClient client ,
408446 ReadOnlySequence < byte > arguments ,
409447 CancellationToken cancellationToken
410448 )
411449 {
450+ var wasMultigraphEnabled = IsMultigraphEnabled ;
451+
412452 // 'Protocol extension: dirtyconfig' (https://guide.munin-monitoring.org/en/latest/plugin/protocol-dirtyconfig.html)
413453 IsDirtyConfigEnabled = SequenceContains ( arguments , "dirtyconfig"u8 ) ;
414454
415- // TODO: multigraph (https://guide.munin-monitoring.org/en/latest/plugin/protocol-multigraph.html)
416- var responseLine = IsDirtyConfigEnabled ? "cap dirtyconfig" : "cap" ;
455+ // 'Protocol extension: multiple graphs from one plugin' (https://guide.munin-monitoring.org/en/latest/plugin/protocol-multigraph.html)
456+ IsMultigraphEnabled = SequenceContains ( arguments , "multigraph"u8 ) ;
457+
458+ if ( IsMultigraphEnabled != wasMultigraphEnabled )
459+ ReinitializePluginDictionary ( ) ;
417460
418461 return SendResponseAsync (
419462 client : client ?? throw new ArgumentNullException ( nameof ( client ) ) ,
420- responseLine : responseLine ,
463+ responseLine : GetCapResponseLine ( IsDirtyConfigEnabled , IsMultigraphEnabled ) ,
421464 cancellationToken : cancellationToken
422465 ) ;
466+
467+ static string GetCapResponseLine ( bool dirtyconfig , bool multigraph )
468+ {
469+ if ( dirtyconfig && multigraph )
470+ return "cap dirtyconfig multigraph" ;
471+
472+ if ( dirtyconfig )
473+ return "cap dirtyconfig" ;
474+
475+ if ( multigraph )
476+ return "cap multigraph" ;
477+
478+ return "cap" ;
479+ }
423480 }
424481
425482 /// <summary>
@@ -437,7 +494,7 @@ CancellationToken cancellationToken
437494 // XXX: ignore [node] arguments
438495 return SendResponseAsync (
439496 client : client ?? throw new ArgumentNullException ( nameof ( client ) ) ,
440- responseLine : string . Join ( " " , profile . PluginProvider . Plugins . Select ( static plugin => plugin . Name ) ) ,
497+ responseLine : joinedPluginList ,
441498 cancellationToken : cancellationToken
442499 ) ;
443500 }
@@ -458,11 +515,8 @@ CancellationToken cancellationToken
458515 throw new ArgumentNullException ( nameof ( client ) ) ;
459516
460517 var queryItem = profile . Encoding . GetString ( arguments ) ;
461- var plugin = profile . PluginProvider . Plugins . FirstOrDefault (
462- plugin => string . Equals ( queryItem , plugin . Name , StringComparison . Ordinal )
463- ) ;
464518
465- if ( plugin is null ) {
519+ if ( ! plugins . TryGetValue ( queryItem , out var plugin ) ) {
466520 await SendResponseAsync (
467521 client ,
468522 ResponseLinesUnknownService ,
@@ -475,11 +529,25 @@ await SendResponseAsync(
475529 var responseLines = responseLineListPool . Take ( ) ;
476530
477531 try {
478- await WriteFetchResponseAsync (
479- plugin . DataSource ,
480- responseLines ,
481- cancellationToken
482- ) . ConfigureAwait ( false ) ;
532+ if ( plugin is IMultigraphPlugin multigraphPlugin ) {
533+ // 'Protocol extension: multiple graphs from one plugin' (https://guide.munin-monitoring.org/en/latest/plugin/protocol-multigraph.html)
534+ foreach ( var subPlugin in multigraphPlugin . Plugins ) {
535+ responseLines . Add ( $ "multigraph { subPlugin . Name } ") ;
536+
537+ await WriteFetchResponseAsync (
538+ subPlugin . DataSource ,
539+ responseLines ,
540+ cancellationToken
541+ ) . ConfigureAwait ( false ) ;
542+ }
543+ }
544+ else {
545+ await WriteFetchResponseAsync (
546+ plugin . DataSource ,
547+ responseLines ,
548+ cancellationToken
549+ ) . ConfigureAwait ( false ) ;
550+ }
483551
484552 responseLines . Add ( "." ) ;
485553
@@ -525,11 +593,8 @@ CancellationToken cancellationToken
525593 throw new ArgumentNullException ( nameof ( client ) ) ;
526594
527595 var queryItem = profile . Encoding . GetString ( arguments ) ;
528- var plugin = profile . PluginProvider . Plugins . FirstOrDefault (
529- plugin => string . Equals ( queryItem , plugin . Name , StringComparison . Ordinal )
530- ) ;
531596
532- if ( plugin is null ) {
597+ if ( ! plugins . TryGetValue ( queryItem , out var plugin ) ) {
533598 return SendResponseAsync (
534599 client ,
535600 ResponseLinesUnknownService ,
@@ -544,16 +609,25 @@ async ValueTask HandleConfigCommandAsyncCore()
544609 var responseLines = responseLineListPool . Take ( ) ;
545610
546611 try {
547- WriteConfigResponse (
548- plugin ,
549- responseLines
550- ) ;
551-
552- if ( IsDirtyConfigEnabled ) {
553- await WriteFetchResponseAsync (
554- dataSource : plugin . DataSource ,
555- responseLines : responseLines ,
556- cancellationToken : cancellationToken
612+ if ( plugin is IMultigraphPlugin multigraphPlugin ) {
613+ // 'Protocol extension: multiple graphs from one plugin' (https://guide.munin-monitoring.org/en/latest/plugin/protocol-multigraph.html)
614+ foreach ( var subPlugin in multigraphPlugin . Plugins ) {
615+ responseLines . Add ( $ "multigraph { subPlugin . Name } ") ;
616+
617+ await WriteConfigResponseAsync (
618+ subPlugin ,
619+ includeFetchResponse : IsDirtyConfigEnabled ,
620+ responseLines ,
621+ cancellationToken
622+ ) . ConfigureAwait ( false ) ;
623+ }
624+ }
625+ else {
626+ await WriteConfigResponseAsync (
627+ plugin ,
628+ includeFetchResponse : IsDirtyConfigEnabled ,
629+ responseLines ,
630+ cancellationToken
557631 ) . ConfigureAwait ( false ) ;
558632 }
559633
@@ -569,6 +643,29 @@ await SendResponseAsync(
569643 responseLineListPool . Return ( responseLines ) ;
570644 }
571645 }
646+
647+ static ValueTask WriteConfigResponseAsync (
648+ IPlugin plugin ,
649+ bool includeFetchResponse ,
650+ List < string > responseLines ,
651+ CancellationToken cancellationToken
652+ )
653+ {
654+ WriteConfigResponse (
655+ plugin ,
656+ responseLines
657+ ) ;
658+
659+ if ( includeFetchResponse ) {
660+ return WriteFetchResponseAsync (
661+ dataSource : plugin . DataSource ,
662+ responseLines : responseLines ,
663+ cancellationToken : cancellationToken
664+ ) ;
665+ }
666+
667+ return default ;
668+ }
572669 }
573670
574671 private static void WriteConfigResponse (
0 commit comments