Merge pull request #1099 from Dexterp37/csharp_counter_type

Bug 1648418 - Implement the C# Counter Metric
This commit is contained in:
Alessio Placitelli 2020-07-28 12:48:04 +02:00 коммит произвёл GitHub
Родитель 03ccfb1d24 1dc6dfdeb3
Коммит 562649698f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 546 добавлений и 12 удалений

Просмотреть файл

@ -4,6 +4,7 @@
* C#
* Add support for Labeled Strings and Labeled Booleans.
* Add support for the Counter metric type and Labeled Counter.
# v31.6.0 (2020-07-24)

Просмотреть файл

@ -105,7 +105,25 @@ assert 0 == metrics.stability.crash_count.test_get_num_recorded_errors(
<div data-lang="C#" class="tab">
TODO. To be implemented in [bug 1648437](https://bugzilla.mozilla.org/show_bug.cgi?id=1648437).
```C#
using static Mozilla.YourApplication.GleanMetrics.Stability;
Stability.crashCount["uncaught_exception"].Add(); // Adds 1 to the "uncaught_exception" counter.
Stability.crashCount["native_code_crash"].Add(3); // Adds 3 to the "native_code_crash" counter.
```
There are test APIs available too:
```C#
using static Mozilla.YourApplication.GleanMetrics.Stability;
// Was anything recorded?
Assert.True(Stability.crashCount["uncaught_exception"].TestHasValue());
Assert.True(Stability.crashCount["native_code_crash"].TestHasValue());
// Do the counters have the expected values?
Assert.Equal(1, Stability.crashCount["uncaught_exception"].TestGetValue());
Assert.Equal(3, Stability.crashCount["native_code_crash"].TestGetValue());
// Were there any invalid labels?
Assert.Equal(0, Stability.crashCount.TestGetNumRecordedErrors(ErrorType.InvalidLabel));
```
</div>

Просмотреть файл

@ -215,6 +215,38 @@ namespace Mozilla.Glean.FFI
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern byte glean_boolean_test_has_value(UInt64 metric_id, string storage_name);
// Counter
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern UInt64 glean_new_counter_metric(
string category,
string name,
string[] send_in_pings,
Int32 send_in_pings_len,
Int32 lifetime,
bool disabled
);
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern void glean_destroy_counter_metric(IntPtr handle);
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern void glean_counter_add(UInt64 metric_id, Int32 amount);
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern Int32 glean_counter_test_get_value(UInt64 metric_id, string storage_name);
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern bool glean_counter_test_has_value(UInt64 metric_id, string storage_name);
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern Int32 glean_counter_test_get_num_recorded_errors(
UInt64 metric_id,
Int32 error_type,
String storage_name
);
// Uuid
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
@ -314,6 +346,29 @@ namespace Mozilla.Glean.FFI
string storage_name
);
// Labeled Counter
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern UInt64 glean_new_labeled_counter_metric(
string category,
string name,
string[] send_in_pings,
Int32 send_in_pings_len,
Int32 lifetime,
bool disabled,
string[] labels,
Int32 label_count
);
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern UInt64 glean_labeled_counter_metric_get(UInt64 handle, string label);
[DllImport(SharedGleanLibrary, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
internal static extern Int32 glean_labeled_counter_test_get_num_recorded_errors(
UInt64 metric_id,
Int32 error_type,
string storage_name
);
// Labeled Boolean
@ -337,7 +392,7 @@ namespace Mozilla.Glean.FFI
UInt64 metric_id,
Int32 error_type,
string storage_name
);
);
// Labeled string

Просмотреть файл

@ -57,7 +57,7 @@ namespace Mozilla.Glean.Private
/// <summary>
/// Set a boolean value.
/// </summary>
/// <param name="value"> This is a user defined boolean value.
/// <param name="value"> This is a user defined boolean value.</param>
public void Set(bool value)
{
if (disabled)

Просмотреть файл

@ -0,0 +1,144 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
using Mozilla.Glean.FFI;
using System;
namespace Mozilla.Glean.Private
{
/// <summary>
/// This implements the developer facing API for recording counter metrics.
///
/// Instances of this class type are automatically generated by the parsers at build time,
/// allowing developers to record values that were previously registered in the metrics.yaml file.
///
/// The internal constructor is only used by [LabeledMetricType] directly.
/// </summary>
public sealed class CounterMetricType : ILabeledSubmetricInterface
{
private bool disabled;
private string[] sendInPings;
private UInt64 handle;
/// <summary>
/// The public constructor used by automatically generated metrics.
/// </summary>
public CounterMetricType(
bool disabled,
string category,
Lifetime lifetime,
string name,
string[] sendInPings
) : this(0, disabled, sendInPings)
{
handle = LibGleanFFI.glean_new_counter_metric(
category: category,
name: name,
send_in_pings: sendInPings,
send_in_pings_len: sendInPings.Length,
lifetime: (int)lifetime,
disabled: disabled);
}
internal CounterMetricType(
UInt64 handle,
bool disabled,
string[] sendInPings
)
{
this.disabled = disabled;
this.sendInPings = sendInPings;
this.handle = handle;
}
/// <summary>
/// Add to counter value.
/// </summary>
/// <param name="amount">this is the amount to increment the counter by, defaulting to
/// 1 if called without parameters.</param>
public void Add(Int32 amount = 1)
{
if (disabled)
{
return;
}
Dispatchers.LaunchAPI(() => {
AddSync(amount);
});
}
/// <summary>
/// Add to counter value synchronously.
///
/// This is only to be used within the Glean SDK.
/// </summary>
/// <param name="amount">this is the amount to increment the counter by, defaulting to 1
/// if called without parameters.</param>
internal void AddSync(Int32 amount)
{
if (disabled)
{
return;
}
LibGleanFFI.glean_counter_add(handle, amount);
}
/// <summary>
/// Tests whether a value is stored for the metric for testing purposes only. This function will
/// attempt to await the last task (if any) writing to the the metric's storage engine before
/// returning a value.
/// </summary>
/// <param name="pingName">represents the name of the ping to retrieve the metric for Defaults
/// to the first value in `sendInPings`</param>
/// <returns>true if metric value exists, otherwise false</returns>
public bool TestHasValue(string pingName = null)
{
Dispatchers.AssertInTestingMode();
string ping = pingName ?? sendInPings[0];
return LibGleanFFI.glean_counter_test_has_value(handle, ping);
}
/// <summary>
/// Returns the stored value for testing purposes only. This function will attempt to await the
/// last task (if any) writing to the the metric's storage engine before returning a value.
/// @throws [NullPointerException] if no value is stored
/// </summary>
/// <param name="pingName">represents the name of the ping to retrieve the metric for.
/// Defaults to the first value in `sendInPings`</param>
/// <returns>value of the stored metric</returns>
/// <exception cref="System.NullReferenceException">Thrown when the metric contains no value</exception>
public Int32 TestGetValue(string pingName = null)
{
Dispatchers.AssertInTestingMode();
if (!TestHasValue(pingName))
{
throw new NullReferenceException();
}
string ping = pingName ?? sendInPings[0];
return LibGleanFFI.glean_counter_test_get_value(handle, ping);
}
/// <summary>
/// Returns the number of errors recorded for the given metric.
/// </summary>
/// <param name="errorType">the type of the error recorded.</param>
/// <param name="pingName">represents the name of the ping to retrieve the metric for.
/// Defaults to the first value in `sendInPings`.</param>
/// <returns>the number of errors recorded for the metric.</returns>
public Int32 TestGetNumRecordedErrors(Testing.ErrorType errorType, string pingName = null)
{
Dispatchers.AssertInTestingMode();
string ping = pingName ?? sendInPings[0];
return LibGleanFFI.glean_counter_test_get_num_recorded_errors(
handle, (int)errorType, ping
);
}
}
}

Просмотреть файл

@ -59,6 +59,9 @@ namespace Mozilla.Glean.Private
case BooleanMetricType _:
metricTypeInstantiator = LibGleanFFI.glean_new_labeled_boolean_metric;
break;
case CounterMetricType _:
metricTypeInstantiator = LibGleanFFI.glean_new_labeled_counter_metric;
break;
case StringMetricType _:
metricTypeInstantiator = LibGleanFFI.glean_new_labeled_string_metric;
break;
@ -110,6 +113,11 @@ namespace Mozilla.Glean.Private
UInt64 handle = LibGleanFFI.glean_labeled_boolean_metric_get(this.handle, label);
return (T)(ILabeledSubmetricInterface)new BooleanMetricType(handle, disabled, sendInPings);
}
case CounterMetricType _:
{
UInt64 handle = LibGleanFFI.glean_labeled_counter_metric_get(this.handle, label);
return (T)(ILabeledSubmetricInterface)new CounterMetricType(handle, disabled, sendInPings);
}
case StringMetricType _:
{
UInt64 handle = LibGleanFFI.glean_labeled_string_metric_get(this.handle, label);
@ -141,6 +149,10 @@ namespace Mozilla.Glean.Private
{
return LibGleanFFI.glean_labeled_boolean_test_get_num_recorded_errors(handle, (int)errorType, ping);
}
case CounterMetricType _:
{
return LibGleanFFI.glean_labeled_counter_test_get_num_recorded_errors(handle, (int)errorType, ping);
}
case StringMetricType _:
{
return LibGleanFFI.glean_labeled_string_test_get_num_recorded_errors(handle, (int)errorType, ping);

Просмотреть файл

@ -0,0 +1,140 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
using Mozilla.Glean.Testing;
using System;
using System.IO;
using Xunit;
using static Mozilla.Glean.Glean;
namespace Mozilla.Glean.Tests.Metrics
{
public class CounterMetricTypeTest
{
public CounterMetricTypeTest()
{
// Get a random test directory just for this single test.
string tempDataDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
// In xUnit, the constructor will be called before each test. This
// feels like a natural place to initialize / reset Glean.
GleanInstance.Reset(
applicationId: "org.mozilla.csharp.tests",
applicationVersion: "1.0-test",
uploadEnabled: true,
configuration: new Configuration(),
dataDir: tempDataDir
);
}
[Fact]
public void APISavesToStorage()
{
Private.CounterMetricType counterMetric = new Private.CounterMetricType(
category: "telemetry",
disabled: false,
lifetime: Private.Lifetime.Application,
name: "counter_metric",
sendInPings: new string[] { "store1" }
);
Assert.False(counterMetric.TestHasValue());
// Add to the counter a couple of times with a little delay. The first call will check
// calling add() without parameters to test increment by 1.
counterMetric.Add();
// Check that the count was incremented and properly recorded.
Assert.True(counterMetric.TestHasValue());
Assert.Equal(1, counterMetric.TestGetValue());
counterMetric.Add(10);
// Check that count was incremented and properly recorded. This second call will check
// calling add() with 10 to test increment by other amount
Assert.True(counterMetric.TestHasValue());
Assert.Equal(11, counterMetric.TestGetValue());
}
[Fact]
public void DisabledCountersMustNotRecordData()
{
Private.CounterMetricType counterMetric = new Private.CounterMetricType(
category: "telemetry",
disabled: true,
lifetime: Private.Lifetime.Application,
name: "counter_metric",
sendInPings: new string[] { "store1" }
);
// Attempt to store the counter.
counterMetric.Add();
// Check that nothing was recorded.
Assert.False(counterMetric.TestHasValue(), "Counters must not be recorded if they are disabled");
}
[Fact]
public void TestGetValueThrows()
{
Private.CounterMetricType counterMetric = new Private.CounterMetricType(
category: "telemetry",
disabled: true,
lifetime: Private.Lifetime.Application,
name: "counter_metric",
sendInPings: new string[] { "store1" }
);
Assert.Throws<NullReferenceException>(() => counterMetric.TestGetValue());
}
[Fact]
public void APISavesToSecondaryPings()
{
Private.CounterMetricType counterMetric = new Private.CounterMetricType(
category: "telemetry",
disabled: false,
lifetime: Private.Lifetime.Application,
name: "counter_metric",
sendInPings: new string[] { "store1", "store2" }
);
// Add to the counter a couple of times with a little delay. The first call will check
// calling add() without parameters to test increment by 1.
counterMetric.Add();
// Check that the count was incremented and properly recorded for the second ping.
Assert.True(counterMetric.TestHasValue("store2"));
Assert.Equal(1, counterMetric.TestGetValue("store2"));
counterMetric.Add(10);
// Check that count was incremented and properly recorded for the second ping.
// This second call will check calling add() with 10 to test increment by other amount
Assert.True(counterMetric.TestHasValue("store2"));
Assert.Equal(11, counterMetric.TestGetValue("store2"));
}
[Fact]
public void NegativeValuesAreNotCounted()
{
Private.CounterMetricType counterMetric = new Private.CounterMetricType(
category: "telemetry",
disabled: false,
lifetime: Private.Lifetime.Application,
name: "counter_metric",
sendInPings: new string[] { "store1" }
);
// Increment to 1 (initial value)
counterMetric.Add();
// Check that the count was incremented
Assert.True(counterMetric.TestHasValue("store1"));
Assert.Equal(1, counterMetric.TestGetValue("store1"));
counterMetric.Add(-10);
// Check that count was NOT incremented.
Assert.True(counterMetric.TestHasValue("store1"));
Assert.Equal(1, counterMetric.TestGetValue("store1"));
Assert.Equal(1, counterMetric.TestGetNumRecordedErrors(ErrorType.InvalidValue));
}
}
}

Просмотреть файл

@ -5,6 +5,7 @@
using Mozilla.Glean.Private;
using Mozilla.Glean.Testing;
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
using static Mozilla.Glean.Glean;
@ -13,11 +14,11 @@ namespace Mozilla.Glean.Tests.Metrics
{
public class LabeledMetricTypeTest
{
// Get a random test directory just for this single test.
string TempDataDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
public LabeledMetricTypeTest()
{
// Get a random test directory just for this single test.
string tempDataDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
// In xUnit, the constructor will be called before each test. This
// feels like a natural place to initialize / reset Glean.
GleanInstance.Reset(
@ -25,38 +26,201 @@ namespace Mozilla.Glean.Tests.Metrics
applicationVersion: "1.0-test",
uploadEnabled: true,
configuration: new Configuration(),
dataDir: tempDataDir
dataDir: TempDataDir
);
}
[Fact]
public void TestLabeledCounterType()
{
// TODO: Placeholder. Implement in bug 1648437 by converting the Kotlin test.
CounterMetricType counterMetric = new CounterMetricType(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" }
);
var labeledCounterMetric = new LabeledMetricType<CounterMetricType>(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" },
submetric: counterMetric
);
labeledCounterMetric["label1"].Add(1);
labeledCounterMetric["label2"].Add(2);
// Record a regular non-labeled counter. This isn't normally
// possible with the generated code because the subMetric is private,
// but it's useful to test here that it works.
counterMetric.Add(3);
Assert.True(labeledCounterMetric["label1"].TestHasValue());
Assert.Equal(1, labeledCounterMetric["label1"].TestGetValue());
Assert.True(labeledCounterMetric["label2"].TestHasValue());
Assert.Equal(2, labeledCounterMetric["label2"].TestGetValue());
Assert.True(counterMetric.TestHasValue());
Assert.Equal(3, counterMetric.TestGetValue());
}
[Fact]
public void TestOtherLabelWithPredefinedLabels()
{
// TODO: Placeholder. Implement in bug 1648437 by converting the Kotlin test.
CounterMetricType counterMetric = new CounterMetricType(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" }
);
var labeledCounterMetric = new LabeledMetricType<CounterMetricType>(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" },
submetric: counterMetric,
labels: new HashSet<string>() { "foo", "bar", "baz" }
);
labeledCounterMetric["foo"].Add(1);
labeledCounterMetric["foo"].Add(2);
labeledCounterMetric["bar"].Add(1);
labeledCounterMetric["not_there"].Add(1);
labeledCounterMetric["also_not_there"].Add(1);
labeledCounterMetric["not_me"].Add(1);
Assert.Equal(3, labeledCounterMetric["foo"].TestGetValue());
Assert.Equal(1, labeledCounterMetric["bar"].TestGetValue());
Assert.False(labeledCounterMetric["baz"].TestHasValue());
// The rest all lands in the __other__ bucket
Assert.Equal(3, labeledCounterMetric["not_there"].TestGetValue());
}
[Fact]
public void TestOtherLabelWithoutPredefinedLabels()
{
// TODO: Placeholder. Implement in bug 1648437 by converting the Kotlin test.
CounterMetricType counterMetric = new CounterMetricType(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" }
);
var labeledCounterMetric = new LabeledMetricType<CounterMetricType>(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" },
submetric: counterMetric
);
for (int i = 0; i <= 20; i++)
{
labeledCounterMetric[$"label_{i}"].Add(1);
}
// Go back and record in one of the real labels again
labeledCounterMetric["label_0"].Add(1);
Assert.Equal(2, labeledCounterMetric["label_0"].TestGetValue());
for (int i = 1; i <= 15; i++)
{
Assert.Equal(1, labeledCounterMetric[$"label_{i}"].TestGetValue());
}
Assert.Equal(5, labeledCounterMetric["__other__"].TestGetValue());
}
[Fact]
public void TestOtherLabelWithoutPredefinedLabelsBeforeGleanInits()
{
// TODO: Placeholder. Implement in bug 1648437 by converting the Kotlin test.
CounterMetricType counterMetric = new CounterMetricType(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" }
);
var labeledCounterMetric = new LabeledMetricType<CounterMetricType>(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" },
submetric: counterMetric
);
// Make sure Glean isn't initialized, and turn task queueing on
GleanInstance.TestDestroyGleanHandle();
Dispatchers.QueueInitialTasks = true;
for (int i = 0; i <= 20; i++)
{
labeledCounterMetric[$"label_{i}"].Add(1);
}
// Go back and record in one of the real labels again
labeledCounterMetric["label_0"].Add(1);
// Initialize glean
GleanInstance.Initialize(
applicationId: "org.mozilla.csharp.tests",
applicationVersion: "1.0-test",
uploadEnabled: true,
configuration: new Configuration(),
dataDir: TempDataDir
);
Assert.Equal(2, labeledCounterMetric["label_0"].TestGetValue());
for (int i = 1; i <= 15; i++)
{
Assert.Equal(1, labeledCounterMetric[$"label_{i}"].TestGetValue());
}
Assert.Equal(5, labeledCounterMetric["__other__"].TestGetValue());
}
[Fact]
public void EnsureInvalidLabelsOnLabeledCounterGoToOther()
{
// TODO: Placeholder. Implement in bug 1648437 by converting the Kotlin test.
CounterMetricType counterMetric = new CounterMetricType(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" }
);
var labeledCounterMetric = new LabeledMetricType<CounterMetricType>(
disabled: false,
category: "telemetry",
lifetime: Lifetime.Application,
name: "labeled_counter_metric",
sendInPings: new string[] { "metrics" },
submetric: counterMetric
);
labeledCounterMetric["notSnakeCase"].Add(1);
labeledCounterMetric[""].Add(1);
labeledCounterMetric["with/slash"].Add(1);
labeledCounterMetric["this_string_has_more_than_thirty_characters"].Add(1);
Assert.Equal(
4,
labeledCounterMetric.TestGetNumRecordedErrors(
ErrorType.InvalidLabel
)
);
Assert.Equal(
4,
labeledCounterMetric["__other__"].TestGetValue()
);
}
[Fact]