Skip to content

Commit 1e79870

Browse files
authored
Merge pull request #29 from vtex/vtexsplunklogger/implement_kpi_concept
Implemented VTEX Kpi concept Small code refactoring for some VTEX log entries
2 parents a0068ac + 11a56b1 commit 1e79870

9 files changed

+421
-21
lines changed
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
3+
namespace Vtex
4+
{
5+
/// <summary>
6+
/// This class contains DateTime extension method to simplify small DateTime usage.
7+
/// </summary>
8+
public static class DateTimeExtensions
9+
{
10+
/// <summary>
11+
/// Return informed DateTime without seconds and milisecond.
12+
/// </summary>
13+
/// <returns>The second mili second.</returns>
14+
/// <param name="baseDate">Base date.</param>
15+
public static DateTime RemoveSecondMiliSecond(this DateTime baseDate)
16+
{
17+
return new DateTime(baseDate.Year, baseDate.Month, baseDate.Day, baseDate.Hour, baseDate.Minute, 0);
18+
}
19+
}
20+
}

src/VTEXSplunkLogger/ILoggerExtensions.cs

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using Microsoft.Extensions.Logging;
34
using Vtex.SplunkLogger;
45

56
namespace Vtex
67
{
78
/// <summary>
8-
/// This class contains ILogger extension method to simplify the process to record a VTEX log.
9+
/// This class contains ILogger extension method to simplify the process to record VTEX Logs and Kpis.
910
/// </summary>
1011
public static class ILoggerExtensions
1112
{
12-
static readonly EventId EmptyEventId = new EventId();
13+
static readonly EventId emptyEventId = new EventId();
14+
static readonly ConcurrentDictionary<ILogger, MetricManager> metricManagers = new ConcurrentDictionary<ILogger, MetricManager>();
15+
16+
static void KpiReady(object sender, VTEXKpiEntry kpiEntry)
17+
{
18+
if(sender is ILogger)
19+
((ILogger)sender).Log(LogLevel.Critical, emptyEventId, kpiEntry, null, null);
20+
}
1321

1422
/// <summary>
1523
/// Log to Splunk.
@@ -25,9 +33,9 @@ public static void DefineVTEXLog(this ILogger logger, LogLevel logLevel, string
2533
{
2634
string formattedMessage = string.Empty;
2735
logger.Log(logLevel,
28-
EmptyEventId,
29-
new VTEXSplunkEntry(workflowType, workflowInstance, account, exception, extraParameters),
30-
exception, (VTEXSplunkEntry arg1, Exception arg2) =>
36+
emptyEventId,
37+
new VTEXLogEntry(workflowType, workflowInstance, account, exception, extraParameters),
38+
exception, (VTEXLogEntry arg1, Exception arg2) =>
3139
{
3240
if (string.IsNullOrWhiteSpace(formattedMessage))
3341
{
@@ -41,5 +49,18 @@ public static void DefineVTEXLog(this ILogger logger, LogLevel logLevel, string
4149
return formattedMessage;
4250
});
4351
}
52+
53+
/// <summary>
54+
/// Generate performance indicator.
55+
/// </summary>
56+
/// <param name="logger">Logger.</param>
57+
/// <param name="kpiName">Kpi name.</param>
58+
/// <param name="kpiValue">Kpi value.</param>
59+
/// <param name="account">Account.</param>
60+
/// <param name="extraParameters">Extra parameters.</param>
61+
public static void DefineVTEXKpi(this ILogger logger, string kpiName, float kpiValue, string account = "", params Tuple<string, string>[] extraParameters)
62+
{
63+
metricManagers.GetOrAdd(logger, new MetricManager(logger, KpiReady)).RegisterKpi(kpiName, kpiValue, account, extraParameters);
64+
}
4465
}
4566
}

src/VTEXSplunkLogger/LoggerFactoryExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Splunk.Configurations;
44
using Splunk;
55

6-
namespace Vtex.SplunkLogger
6+
namespace Vtex
77
{
88
/// <summary>
99
/// This class contains ILoggerFactory extension method to simplify the process to add a Splunk logger provider.

src/VTEXSplunkLogger/MetricManager.cs

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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+
}
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Timers;
4+
5+
namespace Vtex.SplunkLogger
6+
{
7+
/// <summary>
8+
/// This class contains all methods and logics necessary to control a timer that should
9+
/// aways be triggered when minute reach 00 seconds.
10+
/// </summary>
11+
/// <remarks>
12+
/// OnMinuteClockTimer is used to always triggers at same time.
13+
/// </remarks>
14+
class OnMinuteClockTimer
15+
{
16+
Timer SummarizerTimer;
17+
int lastExecutedMinute = -1;
18+
19+
[Category("Behavior")]
20+
[TimersDescription("TimerIntervalElapsed")]
21+
internal event ElapsedEventHandler Elapsed = delegate { };
22+
23+
internal OnMinuteClockTimer()
24+
{
25+
SummarizerTimer = new Timer();
26+
SummarizerTimer.Elapsed += SummarizerTimer_Elapsed;
27+
}
28+
29+
internal void Start()
30+
{
31+
SummarizerTimer.Interval = GetTimerInterval();
32+
SummarizerTimer.Start();
33+
}
34+
35+
internal void Stop()
36+
{
37+
SummarizerTimer.Stop();
38+
}
39+
40+
void SummarizerTimer_Elapsed(object sender, ElapsedEventArgs e)
41+
{
42+
var currentMinute = DateTime.Now.Minute;
43+
if (currentMinute != lastExecutedMinute)
44+
{
45+
Stop();
46+
Elapsed(sender, e);
47+
lastExecutedMinute = currentMinute;
48+
Start();
49+
}
50+
}
51+
52+
double GetTimerInterval()
53+
{
54+
var dateTimeNow = DateTime.Now;
55+
var nextTickDateTime = dateTimeNow.AddMinutes(1).RemoveSecondMiliSecond();
56+
return (nextTickDateTime - dateTimeNow).TotalMilliseconds;
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)