@@ -69,6 +69,7 @@ private sealed class StringListPool(int initialCapacity)
69
69
private readonly string versionInformation ;
70
70
private readonly ArrayBufferWriterPool bufferWriterPool = new ( initialCapacity : 256 ) ;
71
71
private readonly StringListPool responseLineListPool = new ( initialCapacity : 32 ) ;
72
+ private readonly Dictionary < string , IPlugin > plugins = new ( StringComparer . Ordinal ) ;
72
73
73
74
/// <summary>
74
75
/// Gets a value indicating whether the <c>munin master</c> supports
@@ -80,6 +81,21 @@ private sealed class StringListPool(int initialCapacity)
80
81
/// </seealso>
81
82
protected bool IsDirtyConfigEnabled { get ; private set ; }
82
83
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
+
83
99
public MuninProtocolHandler (
84
100
IMuninNodeProfile profile
85
101
)
@@ -88,6 +104,24 @@ IMuninNodeProfile profile
88
104
89
105
banner = $ "# munin node at { profile . HostName } ";
90
106
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
+ ) ;
91
125
}
92
126
93
127
/// <inheritdoc cref="IMuninProtocolHandler.HandleTransactionStartAsync"/>
@@ -397,29 +431,52 @@ CancellationToken cancellationToken
397
431
/// even when <c>dirtyconfig</c> is enabled.
398
432
/// </remarks>
399
433
/// <seealso cref="IsDirtyConfigEnabled"/>
434
+ /// <seealso cref="IsMultigraphEnabled"/>
400
435
/// <seealso href="https://guide.munin-monitoring.org/en/latest/master/network-protocol.html">
401
436
/// Data exchange between master and node - `cap` command
402
437
/// </seealso>
403
438
/// <seealso href="https://guide.munin-monitoring.org/en/latest/plugin/protocol-dirtyconfig.html">
404
439
/// Protocol extension: dirtyconfig
405
440
/// </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>
406
444
protected virtual ValueTask HandleCapCommandAsync (
407
445
IMuninNodeClient client ,
408
446
ReadOnlySequence < byte > arguments ,
409
447
CancellationToken cancellationToken
410
448
)
411
449
{
450
+ var wasMultigraphEnabled = IsMultigraphEnabled ;
451
+
412
452
// 'Protocol extension: dirtyconfig' (https://guide.munin-monitoring.org/en/latest/plugin/protocol-dirtyconfig.html)
413
453
IsDirtyConfigEnabled = SequenceContains ( arguments , "dirtyconfig"u8 ) ;
414
454
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 ( ) ;
417
460
418
461
return SendResponseAsync (
419
462
client : client ?? throw new ArgumentNullException ( nameof ( client ) ) ,
420
- responseLine : responseLine ,
463
+ responseLine : GetCapResponseLine ( IsDirtyConfigEnabled , IsMultigraphEnabled ) ,
421
464
cancellationToken : cancellationToken
422
465
) ;
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
+ }
423
480
}
424
481
425
482
/// <summary>
@@ -437,7 +494,7 @@ CancellationToken cancellationToken
437
494
// XXX: ignore [node] arguments
438
495
return SendResponseAsync (
439
496
client : client ?? throw new ArgumentNullException ( nameof ( client ) ) ,
440
- responseLine : string . Join ( " " , profile . PluginProvider . Plugins . Select ( static plugin => plugin . Name ) ) ,
497
+ responseLine : joinedPluginList ,
441
498
cancellationToken : cancellationToken
442
499
) ;
443
500
}
@@ -458,11 +515,8 @@ CancellationToken cancellationToken
458
515
throw new ArgumentNullException ( nameof ( client ) ) ;
459
516
460
517
var queryItem = profile . Encoding . GetString ( arguments ) ;
461
- var plugin = profile . PluginProvider . Plugins . FirstOrDefault (
462
- plugin => string . Equals ( queryItem , plugin . Name , StringComparison . Ordinal )
463
- ) ;
464
518
465
- if ( plugin is null ) {
519
+ if ( ! plugins . TryGetValue ( queryItem , out var plugin ) ) {
466
520
await SendResponseAsync (
467
521
client ,
468
522
ResponseLinesUnknownService ,
@@ -475,11 +529,25 @@ await SendResponseAsync(
475
529
var responseLines = responseLineListPool . Take ( ) ;
476
530
477
531
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
+ }
483
551
484
552
responseLines . Add ( "." ) ;
485
553
@@ -525,11 +593,8 @@ CancellationToken cancellationToken
525
593
throw new ArgumentNullException ( nameof ( client ) ) ;
526
594
527
595
var queryItem = profile . Encoding . GetString ( arguments ) ;
528
- var plugin = profile . PluginProvider . Plugins . FirstOrDefault (
529
- plugin => string . Equals ( queryItem , plugin . Name , StringComparison . Ordinal )
530
- ) ;
531
596
532
- if ( plugin is null ) {
597
+ if ( ! plugins . TryGetValue ( queryItem , out var plugin ) ) {
533
598
return SendResponseAsync (
534
599
client ,
535
600
ResponseLinesUnknownService ,
@@ -544,16 +609,25 @@ async ValueTask HandleConfigCommandAsyncCore()
544
609
var responseLines = responseLineListPool . Take ( ) ;
545
610
546
611
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
557
631
) . ConfigureAwait ( false ) ;
558
632
}
559
633
@@ -569,6 +643,29 @@ await SendResponseAsync(
569
643
responseLineListPool . Return ( responseLines ) ;
570
644
}
571
645
}
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
+ }
572
669
}
573
670
574
671
private static void WriteConfigResponse (
0 commit comments