-
Couldn't load subscription status.
- Fork 354
[OpAMP.Client] Add heartbeats #3095
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
Changes from 10 commits
e7d70f0
c578b7e
240942a
3bdda26
7ed69cc
fb79a5f
3deeeef
b68429e
96f87a6
838d5cc
8f5cef8
9e105e4
2195988
a532bd2
6684f34
f5637ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| namespace OpenTelemetry.OpAmp.Client.Internal.Services.Heartbeat; | ||
|
|
||
| /// <summary> | ||
| /// Represents the health status of a system component. | ||
| /// </summary> | ||
| internal sealed class ComponentHealthStatus | ||
| { | ||
| public ComponentHealthStatus(string componentName) | ||
| { | ||
| this.ComponentName = componentName; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the name of the component. | ||
| /// </summary> | ||
| public string ComponentName { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the current status of the operation or entity. | ||
| /// </summary> | ||
| public string? Status { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the description of the most recent error encountered. | ||
| /// </summary> | ||
| public string? LastError { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the system is in a healthy state. | ||
| /// </summary> | ||
| public bool IsHealthy { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the start time of the event or operation. | ||
| /// </summary> | ||
| public DateTimeOffset StartTime { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the timestamp indicating the current status update time. | ||
| /// </summary> | ||
| public DateTimeOffset StatusTime { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| namespace OpenTelemetry.OpAmp.Client.Internal.Services.Heartbeat; | ||
|
|
||
| internal sealed class HealthReport | ||
| { | ||
| public ulong StartTime { get; set; } | ||
|
|
||
| public ulong StatusTime { get; set; } | ||
|
|
||
| public bool IsHealthy { get; set; } | ||
|
|
||
| public string? Status { get; set; } | ||
|
|
||
| public string? LastError { get; set; } | ||
|
|
||
| public IList<ComponentHealthStatus> Components { get; } = []; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| using OpenTelemetry.OpAmp.Client.Internal.Listeners; | ||
| using OpenTelemetry.OpAmp.Client.Internal.Listeners.Messages; | ||
| using OpenTelemetry.OpAmp.Client.Internal.Settings; | ||
|
|
||
| namespace OpenTelemetry.OpAmp.Client.Internal.Services.Heartbeat; | ||
|
|
||
| internal sealed class HeartbeatService : IBackgroundService, IOpAmpListener<ConnectionSettingsMessage>, IDisposable | ||
| { | ||
| public const string Name = "heartbeat-service"; | ||
|
|
||
| private readonly FrameDispatcher dispatcher; | ||
| private readonly FrameProcessor processor; | ||
| private readonly CancellationTokenSource cts; | ||
| private Timer? timer; | ||
| private TimeSpan tickInterval; | ||
| private ulong startTime; | ||
| private object timerUpdateLock = new(); | ||
|
|
||
| public HeartbeatService(FrameDispatcher dispatcher, FrameProcessor processor) | ||
| { | ||
| this.dispatcher = dispatcher; | ||
| this.processor = processor; | ||
| this.cts = new CancellationTokenSource(); | ||
|
|
||
| this.processor.Subscribe(this); | ||
| } | ||
|
|
||
| public string ServiceName => Name; | ||
|
|
||
| public void Configure(OpAmpClientSettings settings) | ||
| { | ||
| this.tickInterval = settings.Heartbeat.Interval; | ||
| } | ||
|
|
||
| public void Start() | ||
| { | ||
| this.startTime = GetCurrentTimeInNanoseconds(); | ||
| this.CreateOrUpdateTimer(this.tickInterval); | ||
| } | ||
|
|
||
| public void Stop() | ||
| { | ||
| this.cts.Cancel(); | ||
| this.CreateOrUpdateTimer(Timeout.InfiniteTimeSpan); | ||
| } | ||
|
|
||
| public void HandleMessage(ConnectionSettingsMessage message) | ||
| { | ||
| var newInterval = message.ConnectionSettings.Opamp?.HeartbeatIntervalSeconds ?? 0; | ||
| if (newInterval > 0) | ||
| { | ||
| // TODO: change to proper logging | ||
| Console.WriteLine($"[debug] New heartbeat interval received: {newInterval}s"); | ||
|
|
||
| this.CreateOrUpdateTimer(TimeSpan.FromSeconds(newInterval)); | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| this.processor.Unsubscribe(this); | ||
|
|
||
| this.cts.Dispose(); | ||
| this.timer?.Dispose(); | ||
| } | ||
|
|
||
| private static ulong GetCurrentTimeInNanoseconds() | ||
| { | ||
| return (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000; // Convert to nanoseconds | ||
| } | ||
|
|
||
| private void CreateOrUpdateTimer(TimeSpan interval) | ||
| { | ||
| lock (this.timerUpdateLock) | ||
| { | ||
| this.timer ??= new Timer(this.HeartbeatTick); | ||
|
|
||
| this.timer.Change(interval, interval); | ||
| } | ||
| } | ||
|
|
||
| private async void HeartbeatTick(object? state) | ||
| { | ||
| try | ||
| { | ||
| var report = this.CreateHealthReport(); | ||
|
|
||
| await this.dispatcher.DispatchHeartbeatAsync(report, this.cts.Token) | ||
| .ConfigureAwait(false); | ||
| } | ||
| catch (TaskCanceledException) | ||
| { | ||
| // Ignore task cancellation | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| // TODO: change to proper logging | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentionally left for follow ups? For now, all contrib packages are using EventSources similar to https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/8452d88e83e1c204bd4b43c7c3d8189f94ff87be/src/OpenTelemetry.Resources.Host/HostResourceEventSource.cs (There are even tests for it). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that was my plan as well. Will address logging in a separate PR. |
||
| Console.WriteLine($"[error] Heartbeat error: {ex.Message}"); | ||
| } | ||
| } | ||
|
|
||
| private HealthReport CreateHealthReport() | ||
| { | ||
| return new HealthReport | ||
| { | ||
| StartTime = this.startTime, | ||
| StatusTime = GetCurrentTimeInNanoseconds(), | ||
| IsHealthy = true, | ||
| Status = "OK", | ||
| }; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.