1
+ using System ;
2
+ using System . Collections . Concurrent ;
3
+ using System . Collections . Generic ;
4
+ using System . Text ;
5
+ using System . Threading . Tasks ;
6
+ using System . Timers ;
7
+ using System . Linq ;
8
+ using Microsoft . Extensions . Logging ;
9
+
10
+ namespace Vtex . SplunkLogger
11
+ {
12
+ /// <summary>
13
+ /// This class contains all methods and logics necessary to control kpi process.
14
+ /// </summary>
15
+ /// <remarks>
16
+ /// MetricManager is used to summarize performance indicators during one minute
17
+ /// and them dispatch those entries to Splunk as a "custom" log.
18
+ /// </remarks>
19
+ class MetricManager
20
+ {
21
+ #region [ Private Constants ]
22
+
23
+ internal const string METRIC_SPLIT = "-#-" ;
24
+ internal const string OPTIONAL_SPLIT = ":#:" ;
25
+ internal const string DIMENSION_SPLIT = "|#|" ;
26
+
27
+ #endregion
28
+
29
+ #region [ Private Kpi Ready Event ]
30
+
31
+ event EventHandler < VTEXKpiEntry > kpiReady = delegate { } ;
32
+
33
+ #endregion
34
+
35
+ #region [ Private Static Fields ]
36
+
37
+ static readonly ConcurrentDictionary < string , Tuple < ulong , float > > items = new ConcurrentDictionary < string , Tuple < ulong , float > > ( ) ;
38
+ static readonly ConcurrentDictionary < string , float > itemsMax = new ConcurrentDictionary < string , float > ( ) ;
39
+ static readonly ConcurrentDictionary < string , float > itemsMin = new ConcurrentDictionary < string , float > ( ) ;
40
+
41
+ #endregion
42
+
43
+ #region [ Private Fields ]
44
+
45
+ readonly OnMinuteClockTimer summarizerTimer ;
46
+ readonly ILogger logger ;
47
+
48
+ #endregion
49
+
50
+ #region [ Internal Constructor ]
51
+
52
+ internal MetricManager ( ILogger logger , EventHandler < VTEXKpiEntry > kpiReady )
53
+ {
54
+ this . logger = logger ;
55
+
56
+ summarizerTimer = new OnMinuteClockTimer ( ) ;
57
+ summarizerTimer . Elapsed += SummarizerTimer_Elapsed ;
58
+ summarizerTimer . Start ( ) ;
59
+
60
+ this . kpiReady = kpiReady ;
61
+ }
62
+
63
+ #endregion
64
+
65
+ #region [ Internal Methods ]
66
+
67
+ internal void RegisterKpi ( string kpiName , float kpiValue , string account = "" , params Tuple < string , string > [ ] extraParameters )
68
+ {
69
+
70
+ var extraFields = new Dictionary < string , string > ( ) ;
71
+
72
+ if ( extraParameters != null && extraParameters . Length > 0 )
73
+ extraParameters . ToList ( ) . ForEach ( tuple => extraFields . Add ( tuple . Item1 , tuple . Item2 ) ) ;
74
+
75
+ if ( ! string . IsNullOrWhiteSpace ( account ) )
76
+ extraFields . Add ( "account" , account ) ;
77
+
78
+ RegisterKpi ( kpiName , kpiValue , extraFields ) ;
79
+ }
80
+
81
+ #endregion
82
+
83
+ #region [ Private Methods ]
84
+
85
+ void RegisterKpi ( string metricName , float metricValue , Dictionary < string , string > extraFields )
86
+ {
87
+ var clonedExtraFields = new Dictionary < string , string > ( ) ;
88
+ extraFields . All ( a => { clonedExtraFields . Add ( a . Key , a . Value ) ; return true ; } ) ;
89
+
90
+ string itemKey = GetKey ( metricName , clonedExtraFields ) ;
91
+ Tuple < ulong , float > metricItem = new Tuple < ulong , float > ( 1 , metricValue ) ;
92
+
93
+ items . AddOrUpdate ( itemKey , metricItem , ( key , metric ) =>
94
+ {
95
+ return new Tuple < ulong , float > ( metric . Item1 + metricItem . Item1 , metric . Item2 + metricItem . Item2 ) ;
96
+ } ) ;
97
+
98
+ itemsMax . AddOrUpdate ( itemKey , metricValue , ( key , metric ) =>
99
+ {
100
+ if ( itemsMax . ContainsKey ( itemKey ) )
101
+ {
102
+ float oldValue = itemsMax [ itemKey ] ;
103
+ return metricValue > oldValue ? metricValue : oldValue ;
104
+ }
105
+
106
+ return metric ;
107
+ } ) ;
108
+
109
+ itemsMin . AddOrUpdate ( itemKey , metricValue , ( key , metric ) =>
110
+ {
111
+ if ( itemsMin . ContainsKey ( itemKey ) )
112
+ {
113
+ float oldValue = itemsMin [ itemKey ] ;
114
+ return metricValue < oldValue ? metricValue : oldValue ;
115
+ }
116
+
117
+ return metric ;
118
+ } ) ;
119
+ }
120
+
121
+ void SummarizerTimer_Elapsed ( object sender , ElapsedEventArgs e )
122
+ {
123
+ if ( items . Keys . Count > 0 )
124
+ {
125
+ List < VTEXKpiEntry > kpiMetrics = new List < VTEXKpiEntry > ( ) ;
126
+ Parallel . ForEach ( items . Keys , key =>
127
+ {
128
+ if ( ! string . IsNullOrWhiteSpace ( key ) )
129
+ {
130
+ Tuple < ulong , float > valueTuple = null ;
131
+ items . TryRemove ( key , out valueTuple ) ;
132
+
133
+ float maxValue = 0 ;
134
+ itemsMax . TryRemove ( key , out maxValue ) ;
135
+
136
+ float minValue = 0 ;
137
+ itemsMin . TryRemove ( key , out minValue ) ;
138
+
139
+ var kpiEntry = GenerateEntry ( key , valueTuple , maxValue , minValue ) ;
140
+ kpiReady ( logger , kpiEntry ) ;
141
+ }
142
+ } ) ;
143
+ }
144
+ }
145
+
146
+ VTEXKpiEntry GenerateEntry ( string key , Tuple < ulong , float > valueTuple , float maxValue , float minValue )
147
+ {
148
+ string metricName = string . Empty ;
149
+ Dictionary < string , string > extraFields = null ;
150
+ RetreiveKeyItems ( key , out metricName , out extraFields ) ;
151
+
152
+ VTEXKpiEntry entry = new VTEXKpiEntry ( metricName )
153
+ {
154
+ Count = valueTuple . Item1 ,
155
+ Name = metricName ,
156
+ Sum = valueTuple . Item2 ,
157
+ Max = maxValue ,
158
+ Min = minValue
159
+ } ;
160
+
161
+ if ( extraFields != null && extraFields . Count > 0 )
162
+ {
163
+ if ( extraFields . ContainsKey ( "account" ) )
164
+ {
165
+ entry . Account = extraFields [ "account" ] ;
166
+ extraFields . Remove ( "account" ) ;
167
+ }
168
+ }
169
+
170
+ entry . ExtraParameters = extraFields ;
171
+
172
+ return entry ;
173
+ }
174
+
175
+ void RetreiveKeyItems ( string key , out string metricName , out Dictionary < string , string > customFields )
176
+ {
177
+ metricName = string . Empty ;
178
+ customFields = null ;
179
+ string [ ] parts = key . Split ( new string [ ] { METRIC_SPLIT } , StringSplitOptions . RemoveEmptyEntries ) ;
180
+ metricName = parts [ 0 ] ;
181
+ if ( parts . Length > 1 )
182
+ {
183
+ customFields = new Dictionary < string , string > ( ) ;
184
+ string [ ] metricParts = parts [ 1 ] . Split ( new string [ ] { OPTIONAL_SPLIT } , StringSplitOptions . RemoveEmptyEntries ) ;
185
+ string [ ] dimensionParts = null ;
186
+ foreach ( string metricPart in metricParts )
187
+ {
188
+ dimensionParts = metricPart . Split ( new string [ ] { DIMENSION_SPLIT } , StringSplitOptions . RemoveEmptyEntries ) ;
189
+ if ( dimensionParts . Length == 2 )
190
+ customFields . Add ( dimensionParts [ 0 ] , dimensionParts [ 1 ] ) ;
191
+ else
192
+ customFields . Add ( dimensionParts [ 0 ] , string . Empty ) ;
193
+ }
194
+ }
195
+ }
196
+
197
+ string GetKey ( string metricName , Dictionary < string , string > extraFields = null )
198
+ {
199
+ if ( extraFields != null && extraFields . Count > 0 )
200
+ {
201
+ StringBuilder stringBuilder = new StringBuilder ( ) ;
202
+
203
+ foreach ( KeyValuePair < string , string > item in extraFields )
204
+ {
205
+ stringBuilder . AppendFormat ( "{0}{1}{2}" , item . Key , DIMENSION_SPLIT , item . Value ) ;
206
+ stringBuilder . Append ( OPTIONAL_SPLIT ) ;
207
+ }
208
+ string itemString = stringBuilder . ToString ( ) ;
209
+ itemString = string . Format ( "{0}{1}{2}" , metricName , METRIC_SPLIT , itemString . Remove ( itemString . Length - OPTIONAL_SPLIT . Length , OPTIONAL_SPLIT . Length ) ) ;
210
+ return itemString ;
211
+ }
212
+
213
+ return metricName ;
214
+ }
215
+
216
+ #endregion
217
+ }
218
+ }
0 commit comments