Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Metrics #248

Merged
merged 45 commits into from
Jun 11, 2019
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c367be9
Add MetricsCollector and impl. system.process.cpu.total.norm.pc - WIP
gregkalapos May 7, 2019
690b520
Implement collecting multiple metrics for 1 timestamp
gregkalapos May 7, 2019
d1152fb
Metrics - Add System.Management reference
gregkalapos May 8, 2019
36dc0fa
Add more memory metrics
gregkalapos May 8, 2019
8e251b2
Change TotalProcessorTime calculation
gregkalapos May 9, 2019
f0d67c6
Add metrics benchmark
gregkalapos May 9, 2019
31b7856
Add total CPU x-plat and windows implementation
gregkalapos May 9, 2019
fa8c20f
Use GlobalMemoryStatusEx to get Total and Avail. Mem on Windows
gregkalapos May 10, 2019
e29093a
Add GetProcessWorkingSetAndVirtualMemory function
gregkalapos May 10, 2019
81f9f84
Update src/Elastic.Apm/Metrics/MetricsCollector.cs
gregkalapos May 11, 2019
846c47d
WIP x-plat CPU metrics
gregkalapos May 12, 2019
7a34bbb
Cleanup and add metrics to Public API
gregkalapos May 31, 2019
abb0604
Add MetricsInterval config
gregkalapos May 31, 2019
281377a
Move MetricsCollector to AgentComponents
gregkalapos May 31, 2019
3c926d2
Update src/Elastic.Apm/Metrics/MetricsCollector.cs
gregkalapos May 31, 2019
1750475
Add FakeMetricsCollector for tests that don't rely on metrics
gregkalapos Jun 3, 2019
4b14ccd
Introduce IMetricsProvider
gregkalapos Jun 3, 2019
099b718
Metrics: Add test with real agent, add comments to public classes/met…
gregkalapos Jun 4, 2019
264e57e
Code cleanup
gregkalapos Jun 4, 2019
e4bbb06
Update test/Elastic.Apm.Tests/MetricsTests.cs
gregkalapos Jun 4, 2019
736e2f7
Change CPU usage calculation
gregkalapos Jun 4, 2019
2184277
Refactor parsing code
SergeyKleyman Jun 5, 2019
64ff726
Replace usages of default value with a named constant
SergeyKleyman Jun 5, 2019
ae24035
Use pattern matching instead of ifs
SergeyKleyman Jun 5, 2019
dfc56ea
Any negative value for MetricsInterval should be treated as 0
SergeyKleyman Jun 5, 2019
9ac03e5
Add unit test to make sure DefaultValues.MetricsInterval and DefaultV…
SergeyKleyman Jun 5, 2019
31e83a5
Incorporate review feedback
gregkalapos Jun 5, 2019
8f8accb
Metrics: avoid reentrancy when collecting metrics
gregkalapos Jun 5, 2019
b25b8e4
Fix typo
gregkalapos Jun 6, 2019
b8f109d
CPU metrics: handle the unlikely case of totalMsPassed being 0
gregkalapos Jun 6, 2019
2fd21c0
Update src/Elastic.Apm/Metrics/Windows/GlobalMemoryStatusEx.cs
gregkalapos Jun 6, 2019
0140a2c
ProcessTotalCpuTimeProvider: return value also in the first call
gregkalapos Jun 6, 2019
60ba819
Use /proc/stat on Linux to report system CPU usage
gregkalapos Jun 6, 2019
f0d1dd1
Update src/Elastic.Apm/Report/Serialization/MetricSetConverter.cs
gregkalapos Jun 6, 2019
7fb1ad1
MetricsCollector: move new List<IMetricsProvider> after interval check
gregkalapos Jun 7, 2019
8cb0afa
Metrics: make things internal
gregkalapos Jun 7, 2019
85aaa20
Metrics: remove x-plat system CPU metrics
gregkalapos Jun 7, 2019
5595d10
Update src/Elastic.Apm/Metrics/MetricsProvider/SystemTotalCpuProvider.cs
gregkalapos Jun 7, 2019
d175b0b
Update src/Elastic.Apm/Metrics/MetricsProvider/SystemTotalCpuProvider.cs
gregkalapos Jun 7, 2019
790a4f2
ProcessTotalCpuTimeProvider: add comment explaning the timeframes we …
gregkalapos Jun 9, 2019
a8b272f
Update test/Elastic.Apm.Tests/MetricsTests.cs
gregkalapos Jun 9, 2019
fb96b54
/proc/stat: parse values to long, trim empty spaces dynamically after…
gregkalapos Jun 10, 2019
63c1a72
SystemTotalCpuProvider: Inject StreamReader to test parsing logic
gregkalapos Jun 10, 2019
6929bdd
Fix failing SystemCpu on Windows
gregkalapos Jun 10, 2019
ab37547
SystemCPU on Windows: move 1. perf. counter call to .ctor
gregkalapos Jun 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal static class Keys
internal const string SecretToken = "ElasticApm:SecretToken";
internal const string CaptureHeaders = "ElasticApm:CaptureHeaders";
internal const string TransactionSampleRate = "ElasticApm:TransactionSampleRate";
internal const string MetricsInterval = "ElasticApm:MetricsInterval";
}

private readonly IConfiguration _configuration;
Expand Down Expand Up @@ -60,6 +61,8 @@ public LogLevel LogLevel

public double TransactionSampleRate => ParseTransactionSampleRate(ReadFallBack(Keys.TransactionSampleRate, ConfigConsts.EnvVarNames.TransactionSampleRate));

public double MetricsIntervalInMillisecond => ParseMetricsInterval(ReadFallBack(Keys.MetricsInterval , ConfigConsts.EnvVarNames.MetricsInterval));

private ConfigurationKeyValue Read(string key) => Kv(key, _configuration[key], Origin);

private ConfigurationKeyValue ReadFallBack(string key, string fallBackEnvVarName)
Expand Down
2 changes: 0 additions & 2 deletions src/Elastic.Apm/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Elastic.Apm.Config;
using Elastic.Apm.DiagnosticSource;
using Elastic.Apm.Logging;
using Elastic.Apm.Model;
using Elastic.Apm.Report;

//TODO: It'd be nice to move this into the .csproj
Expand Down Expand Up @@ -106,7 +105,6 @@ public static class Agent
public static void Setup(AgentComponents agentComponents)
{
if (Lazy.IsValueCreated) throw new Exception("The singleton APM agent has already been instantiated and can no longer be configured");

_components = agentComponents;
}
}
Expand Down
28 changes: 22 additions & 6 deletions src/Elastic.Apm/AgentComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Elastic.Apm.Api;
using Elastic.Apm.Config;
using Elastic.Apm.Logging;
using Elastic.Apm.Model;
using Elastic.Apm.Metrics;
using Elastic.Apm.Report;

namespace Elastic.Apm
Expand All @@ -15,23 +15,34 @@ public AgentComponents(
IPayloadSender payloadSender = null
)
{

Logger = logger ?? ConsoleLogger.LoggerOrDefault(configurationReader?.LogLevel);
ConfigurationReader = configurationReader ?? new EnvironmentConfigurationReader(Logger);
Service = Service.GetDefaultService(ConfigurationReader);
PayloadSender = payloadSender ?? new PayloadSenderV2(Logger, ConfigurationReader, Service);

Service = Service.GetDefaultService(ConfigurationReader);
MetricsCollector = new MetricsCollector(Logger, PayloadSender, ConfigurationReader);
MetricsCollector.StartCollecting();

PayloadSender = payloadSender ?? new PayloadSenderV2(Logger, ConfigurationReader, Service);
TracerInternal = new Tracer(Logger, Service, PayloadSender, ConfigurationReader);
TransactionContainer = new TransactionContainer();
}

internal AgentComponents(
IMetricsCollector metricsCollector,
IApmLogger logger = null,
IConfigurationReader configurationReader = null,
IPayloadSender payloadSender = null
) : this(logger, configurationReader, payloadSender)
=> (MetricsCollector = metricsCollector ?? new MetricsCollector(Logger, PayloadSender, ConfigurationReader)).StartCollecting();

public IConfigurationReader ConfigurationReader { get; }

public IApmLogger Logger { get; }

public IPayloadSender PayloadSender { get; }

private IMetricsCollector MetricsCollector { get; }

/// <summary>
/// Identifies the monitored service. If this remains unset the agent
/// automatically populates it based on the entry assembly.
Expand All @@ -47,9 +58,14 @@ public AgentComponents(

public void Dispose()
{
if (PayloadSender is IDisposable disposable)
if (MetricsCollector is IDisposable disposableMetricsCollector)
{
disposableMetricsCollector.Dispose();
}

if (PayloadSender is IDisposable disposablePayloadSender)
{
disposable?.Dispose();
disposablePayloadSender.Dispose();
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/Elastic.Apm/Api/IMetricSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;

namespace Elastic.Apm.Api
{
/// <summary>
/// Data captured by the agent representing a metric occurring in a monitored service
/// </summary>
public interface IMetricSet
SergeyKleyman marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// List of captured metrics as key - value pairs
/// </summary>
IEnumerable<MetricSample> Samples { get; set; }

/// <summary>
/// Number of milliseconds in unix time
/// </summary>
long TimeStamp { get; set; }
}
}
18 changes: 18 additions & 0 deletions src/Elastic.Apm/Api/MetricSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using Elastic.Apm.Helpers;

namespace Elastic.Apm.Api
{
/// <summary>
/// A single metric sample.
/// </summary>
public class MetricSample
{
public MetricSample(string key, double value)
=> KeyValue = new KeyValuePair<string, double>(key, value);

internal KeyValuePair<string, double> KeyValue { get; set; }

public override string ToString() => new ToStringBuilder(nameof(MetricSample)) { { KeyValue.Key, KeyValue.Value }, }.ToString();
}
}
80 changes: 80 additions & 0 deletions src/Elastic.Apm/Config/AbstractConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Elastic.Apm.Logging;

Expand Down Expand Up @@ -111,6 +112,85 @@ bool TryParseUri(string u, out Uri uri)
}
}

protected double ParseMetricsInterval(ConfigurationKeyValue kv)
{
string value;
if (kv == null || string.IsNullOrWhiteSpace(kv.Value))
value = ConfigConsts.DefaultValues.MetricsInterval;
else
value = kv.Value;

if (!TryParseTimeInterval(value, out var valueInMilliseconds))
{
Logger?.Error()
?.Log("Failed to parse provided metrics interval `{ProvidedMetricsInterval}' - " +
"using default: {DefaultMetricsInterval}",
value,
ConfigConsts.DefaultValues.MetricsInterval);
return ConfigConsts.DefaultValues.MetricsIntervalInMilliseconds;
}

// ReSharper disable once CompareOfFloatsByEqualityOperator - we compare to exactly zero here
if (valueInMilliseconds == 0)
return valueInMilliseconds;

if (valueInMilliseconds < 0)
{
Logger?.Error()
?.Log("Provided metrics interval `{ProvidedMetricsInterval}' is negative - " +
"metrics collection will be disabled",
value);
return 0;
}

if (valueInMilliseconds < ConfigConsts.Constraints.MinMetricsIntervalInMillisecond)
{
Logger?.Error()
?.Log("Provided metrics interval `{ProvidedMetricsInterval}' is smaller than allowed minimum: {MinProvidedMetricsInterval}ms - " +
"metrics collection will be disabled",
value,
ConfigConsts.Constraints.MinMetricsIntervalInMillisecond);
return 0;
}

return valueInMilliseconds;
}

private bool TryParseTimeInterval(String valueAsString, out double valueInMilliseconds)
SergeyKleyman marked this conversation as resolved.
Show resolved Hide resolved
{
switch (valueAsString)
{
case string _ when valueAsString.Length >= 2 && valueAsString.Substring(valueAsString.Length - 2).ToLower() == "ms":
return TryParseFloatingPoint(valueAsString.Substring(0, valueAsString.Length - 2), out valueInMilliseconds);

case string _ when char.ToLower(valueAsString.Last()) == 's':
if (!TryParseFloatingPoint(valueAsString.Substring(0, valueAsString.Length - 1), out var valueInSeconds))
{
valueInMilliseconds = 0;
return false;
}
valueInMilliseconds = TimeSpan.FromSeconds(valueInSeconds).TotalMilliseconds;
return true;

case string _ when char.ToLower(valueAsString.Last()) == 'm':
if (!TryParseFloatingPoint(valueAsString.Substring(0, valueAsString.Length - 1), out var valueInMinutes))
{
valueInMilliseconds = 0;
return false;
}
valueInMilliseconds = TimeSpan.FromMinutes(valueInMinutes).TotalMilliseconds;
return true;
default:
if (!TryParseFloatingPoint(valueAsString, out var valueInSecondsNoUnits))
{
valueInMilliseconds = 0;
return false;
}
valueInMilliseconds = TimeSpan.FromSeconds(valueInSecondsNoUnits).TotalMilliseconds;
return true;
}
}

protected virtual string DiscoverServiceName()
{
var entryAssemblyName = Assembly.GetEntryAssembly()?.GetName();
Expand Down
8 changes: 8 additions & 0 deletions src/Elastic.Apm/Config/ConfigConsts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ public static class EnvVarNames
public const string SecretToken = "ELASTIC_APM_SECRET_TOKEN";
public const string CaptureHeaders = "ELASTIC_APM_CAPTURE_HEADERS";
public const string TransactionSampleRate = "ELASTIC_APM_TRANSACTION_SAMPLE_RATE";
public const string MetricsInterval = "ELASTIC_APM_METRICS_INTERVAL";
}

public static class DefaultValues
{
public const double TransactionSampleRate = 1.0;
public const string UnknownServiceName = "unknown";
public const double MetricsIntervalInMilliseconds = 30 * 1000;
public const string MetricsInterval = "30s";
}

public static class Constraints
{
public const double MinMetricsIntervalInMillisecond = 1000;
}

public static Uri DefaultServerUri => new Uri("http://localhost:8200");
Expand Down
2 changes: 2 additions & 0 deletions src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public EnvironmentConfigurationReader(IApmLogger logger = null) : base(logger) {

public double TransactionSampleRate => ParseTransactionSampleRate(Read(ConfigConsts.EnvVarNames.TransactionSampleRate));

public double MetricsIntervalInMillisecond => ParseMetricsInterval(Read(ConfigConsts.EnvVarNames.MetricsInterval));

private static ConfigurationKeyValue Read(string key) =>
new ConfigurationKeyValue(key, Environment.GetEnvironmentVariable(key), Origin);
}
Expand Down
1 change: 1 addition & 0 deletions src/Elastic.Apm/Config/IAgentConfigReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public interface IConfigurationReader
string SecretToken { get; }
bool CaptureHeaders { get; }
double TransactionSampleRate { get; }
double MetricsIntervalInMillisecond { get; }
}
}
1 change: 1 addition & 0 deletions src/Elastic.Apm/Elastic.Apm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<!-- TODO: Are we ok with this reference/version? -->
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="4.0.0" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="4.5.0" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion src/Elastic.Apm/Logging/IApmLoggingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ internal readonly struct MaybeLogger
public void Log(string message, params object[] args) => _logger.DoLog(_level, message, null, args);

public void LogException(Exception exception, string message, params object[] args) =>
_logger.DoLog(_level, message, exception, args, exception.GetType().FullName, exception.Message);
_logger.DoLog(_level, message, exception, args);

public void LogExceptionWithCaller(Exception exception,
[CallerMemberName] string method = "",
Expand Down
13 changes: 13 additions & 0 deletions src/Elastic.Apm/Metrics/IMetricsCollector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Elastic.Apm.Metrics
{
/// <summary>
/// Defines how the agent collects metrics.
/// </summary>
internal interface IMetricsCollector
{
/// <summary>
/// After calling this method, the <see cref="IMetricsCollector"/> starts collecting metrics
/// </summary>
void StartCollecting();
}
}
32 changes: 32 additions & 0 deletions src/Elastic.Apm/Metrics/IMetricsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using Elastic.Apm.Api;

namespace Elastic.Apm.Metrics
{
/// <summary>
/// Defines an interface that every class, which provides some metric value, should implement.
/// This interface is known to the <see cref="MetricsCollector" /> type and you
/// can implement new providers for other metrics by implementing this interface
/// and adding it to <see cref="MetricsCollector" />.
/// </summary>
internal interface IMetricsProvider
{
/// <summary>
/// Stores the number of calls to the <see cref="GetSamples"/> method when it either returned null or an empty list.
/// This is used by <see cref="MetricsCollector"/>
/// </summary>
int ConsecutiveNumberOfFailedReads { get; set; }
SergeyKleyman marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// The name that refers to the provider in the logs. E.g. "total process CPU time".
/// Make sure this is human understandable and tells the reader what type of value this provider is intended to provide.
/// </summary>
string DbgName { get; }

/// <summary>
/// The main part of the provider, the implementor should do the work to read the value(s) of the given metric(s) in this method.
/// </summary>
/// <returns>The key and the value of the metric(s)</returns>
IEnumerable<MetricSample> GetSamples();
}
}
19 changes: 19 additions & 0 deletions src/Elastic.Apm/Metrics/MetricSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using Elastic.Apm.Api;
using Elastic.Apm.Report.Serialization;
using Newtonsoft.Json;

namespace Elastic.Apm.Metrics
{
[JsonConverter(typeof(MetricSetConverter))]
internal class MetricSet : IMetricSet
{
public MetricSet(long timeStamp, List<MetricSample> samples)
=> (TimeStamp, Samples) = (timeStamp, samples);

public IEnumerable<MetricSample> Samples { get; set; }

[JsonProperty("timestamp")]
public long TimeStamp { get; set; }
}
}
Loading