-
Notifications
You must be signed in to change notification settings - Fork 469
Add AppInsights module to publish metrics #11365
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
Open
jviau
wants to merge
10
commits into
dev
Choose a base branch
from
u/jviau/app-insights-meter-listener
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
1a80a8c
Publish Meter-based metrics to AppInsights SDK
jviau 9c79dad
Refactor Metric construction
jviau 5a25ad3
Publish Meter-based metrics to AppInsights SDK
jviau bf2b2c6
Refactor Metric construction
jviau 514ad74
Add tests for AppInsightsMetricExporter
jviau 7d7d97a
Address PR comments, fix build
jviau 1d0d118
Add EventSource for ApplicationInsightsMetricExporter
jviau d6c4fdc
Update EventSource name
jviau e1efb97
Add more event logs, collect on dispose
jviau 45d4d57
Rename EventSource
jviau File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
51 changes: 51 additions & 0 deletions
51
src/WebJobs.Script/Diagnostics/ApplicationInsightsMetricExporter.EventSource.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Diagnostics.Metrics; | ||
using System.Diagnostics.Tracing; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.Diagnostics | ||
{ | ||
public sealed partial class ApplicationInsightsMetricExporter | ||
{ | ||
[EventSource(Name = $"{ScriptConstants.HostEventSourcePrefix}{nameof(ApplicationInsightsMetricExporter)}")] | ||
private sealed class Events : EventSource | ||
{ | ||
public static readonly Events Log = new(); | ||
|
||
private Events() | ||
{ | ||
} | ||
|
||
[Event(1, Message = "Failed to collect observable instruments: {0}", Level = EventLevel.Error)] | ||
public void FailedToCollectInstruments(Exception error) => WriteEvent(1, Format(error)); | ||
|
||
[Event(2, Message = "Begin collecting observable instruments.")] | ||
public void BeginCollectObservables() => WriteEvent(2); | ||
|
||
[Event(3, Message = "End collecting observable instruments.")] | ||
public void EndCollectObservables() => WriteEvent(3); | ||
|
||
[Event(4, Message = "Meter listening started.")] | ||
public void MeterListeningStarted() => WriteEvent(4); | ||
|
||
[Event(5, Message = "Meter listening stopped.")] | ||
public void MeterListeningStopped() => WriteEvent(5); | ||
|
||
[Event(6, Message = "Subscribed to instrument {0} on meter {1}.")] | ||
public void SubscribedToInstrument(Instrument instrument) => WriteEvent(6, instrument.Name, instrument.Meter.Name); | ||
|
||
[Event(7, Message = "Error starting metric listener: {0}", Level = EventLevel.Error)] | ||
public void ErrorStartingMetricListener(Exception error) => WriteEvent(7, Format(error)); | ||
|
||
[Event(8, Message = "Flushed TelemetryClient.")] | ||
public void FlushedTelemetryClient() => WriteEvent(8); | ||
|
||
private static string Format(Exception error) | ||
{ | ||
return $"{error.GetType().FullName}: {error.Message}"; | ||
} | ||
} | ||
} | ||
} |
164 changes: 164 additions & 0 deletions
164
src/WebJobs.Script/Diagnostics/ApplicationInsightsMetricExporter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
#nullable enable | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.Metrics; | ||
using System.Numerics; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.ApplicationInsights; | ||
using Microsoft.ApplicationInsights.Extensibility; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.Diagnostics | ||
{ | ||
/// <summary> | ||
/// A meter listener which exports metrics to Application Insights. | ||
/// </summary> | ||
public sealed partial class ApplicationInsightsMetricExporter : ITelemetryModule, IAsyncDisposable | ||
{ | ||
private readonly MeterListener _listener; | ||
private readonly ApplicationInsightsMetricExporterOptions _options; | ||
private readonly CancellationTokenSource _shutdown = new(); | ||
|
||
private Task _exportTask = Task.CompletedTask; | ||
private TelemetryClient _client = null!; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="ApplicationInsightsMetricExporter"/> class. | ||
/// </summary> | ||
/// <param name="options">The options.</param> | ||
public ApplicationInsightsMetricExporter(IOptions<ApplicationInsightsMetricExporterOptions> options) | ||
{ | ||
ArgumentNullException.ThrowIfNull(options); | ||
|
||
_options = options.Value; | ||
_listener = new() | ||
{ | ||
InstrumentPublished = (instrument, listener) => | ||
{ | ||
if (_options.ShouldListenTo(instrument)) | ||
{ | ||
Events.Log.SubscribedToInstrument(instrument); | ||
listener.EnableMeasurementEvents(instrument, this); | ||
} | ||
}, | ||
}; | ||
|
||
// All of the supported instrument value types. | ||
_listener.SetMeasurementEventCallback(CreateCallback<byte>()); | ||
_listener.SetMeasurementEventCallback(CreateCallback<short>()); | ||
_listener.SetMeasurementEventCallback(CreateCallback<int>()); | ||
_listener.SetMeasurementEventCallback(CreateCallback<long>()); | ||
_listener.SetMeasurementEventCallback(CreateCallback<float>()); | ||
_listener.SetMeasurementEventCallback(CreateCallback<double>()); | ||
_listener.SetMeasurementEventCallback(CreateCallback<decimal>()); | ||
} | ||
|
||
/// <summary> | ||
/// Initializes this module, starting the meter listener and exporting process. | ||
/// </summary> | ||
/// <param name="configuration">The telemetry configuration.</param> | ||
public void Initialize(TelemetryConfiguration configuration) | ||
{ | ||
ArgumentNullException.ThrowIfNull(configuration); | ||
ObjectDisposedException.ThrowIf(_shutdown.IsCancellationRequested, this); | ||
|
||
try | ||
{ | ||
_client = new TelemetryClient(configuration); | ||
_listener.Start(); | ||
_exportTask = CollectAsync(_shutdown.Token); | ||
Events.Log.MeterListeningStarted(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
Events.Log.ErrorStartingMetricListener(ex); | ||
throw; | ||
} | ||
} | ||
|
||
/// <inheritdoc /> | ||
public async ValueTask DisposeAsync() | ||
{ | ||
Events.Log.MeterListeningStopped(); | ||
|
||
await _shutdown.CancelNoThrowAsync(); | ||
await _exportTask.ConfigureAwait(false); | ||
jviau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
CollectCore(); // collect one more time to ensure we get the last set of values. | ||
_listener.Dispose(); | ||
|
||
if (_client is { } client) | ||
{ | ||
await client.FlushAsync(default).ConfigureAwait(false); | ||
jviau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Events.Log.FlushedTelemetryClient(); | ||
} | ||
|
||
_shutdown.Dispose(); | ||
} | ||
|
||
/// <summary> | ||
/// Flushes the internal client. | ||
/// </summary> | ||
/// <remarks> | ||
/// Primarily for testing purposes. | ||
/// </remarks> | ||
internal void Flush() => _client.Flush(); | ||
|
||
private static MeasurementCallback<T> CreateCallback<T>() | ||
where T : struct, INumber<T>, IConvertible | ||
{ | ||
return (instrument, value, tags, state) => | ||
{ | ||
if (state is not ApplicationInsightsMetricExporter listener) | ||
{ | ||
return; | ||
} | ||
|
||
listener.Publish(instrument, value.ToDouble(null), tags); | ||
}; | ||
} | ||
|
||
private async Task CollectAsync(CancellationToken cancellation) | ||
jviau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
while (!cancellation.IsCancellationRequested) | ||
{ | ||
try | ||
{ | ||
CollectCore(); | ||
await Task.Delay(_options.CollectInterval, cancellation); | ||
} | ||
catch (OperationCanceledException) | ||
{ | ||
} | ||
} | ||
} | ||
|
||
private void CollectCore() | ||
{ | ||
try | ||
{ | ||
Events.Log.BeginCollectObservables(); | ||
_listener.RecordObservableInstruments(); | ||
Events.Log.EndCollectObservables(); | ||
} | ||
catch (Exception ex) when (!ex.IsFatal()) | ||
{ | ||
Events.Log.FailedToCollectInstruments(ex); | ||
} | ||
} | ||
|
||
private void Publish(Instrument instrument, double value, ReadOnlySpan<KeyValuePair<string, object?>> tags) | ||
{ | ||
if (instrument is null) | ||
{ | ||
return; | ||
} | ||
|
||
jviau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_client.TrackInstrument(instrument, value, tags); | ||
} | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
src/WebJobs.Script/Diagnostics/ApplicationInsightsMetricExporterOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
#nullable enable | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.Metrics; | ||
using Microsoft.Azure.WebJobs.Script.Metrics; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.Diagnostics | ||
{ | ||
/// <summary> | ||
/// Options for <see cref="ApplicationInsightsMetricExporter"/>. | ||
/// </summary> | ||
public class ApplicationInsightsMetricExporterOptions | ||
{ | ||
// AppInsights has some metrics which are already emitted a different way. | ||
// We ignore them here to ensure we don't duplicate the metric. | ||
private static readonly HashSet<string> IgnoredInstruments = | ||
[ | ||
HostMetrics.FaasInvokeDuration, | ||
]; | ||
|
||
/// <summary> | ||
/// Gets the set of meter names to listen to. | ||
/// </summary> | ||
public ISet<string> Meters { get; } = new HashSet<string>(StringComparer.Ordinal); | ||
|
||
/// <summary> | ||
/// Gets or sets the interval to collect meter values. Default is 30 seconds. | ||
/// </summary> | ||
/// <remarks> | ||
/// This is the interval at which values for metrics will be tracked on the Application Insights SDK. This is | ||
/// NOT the export interval. Application Insights SDK will export tracked values based on its own internal | ||
/// schedule. | ||
/// </remarks> | ||
public TimeSpan CollectInterval { get; set; } = TimeSpan.FromSeconds(30); | ||
|
||
/// <summary> | ||
/// Determines if given instrument should be listened to. | ||
/// </summary> | ||
/// <param name="instrument">The instrument.</param> | ||
/// <returns><c>true</c> if should be listened to, <c>false</c> otherwise.</returns> | ||
public bool ShouldListenTo(Instrument instrument) | ||
{ | ||
ArgumentNullException.ThrowIfNull(instrument); | ||
|
||
// TODO: consider allowing wildcards or regex | ||
// For now, just exact match on meter name | ||
return Meters.Contains(instrument.Meter.Name) && !IgnoredInstruments.Contains(instrument.Name); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.