···18181919public static class LdSignature
2020{
2121- private static readonly ActivitySource ActivitySource = new("Iceshrimp.NET/LdSignature", VersionHelpers.VersionInfo.Value.Version);
2222-2321 public static Task<bool> VerifyAsync(JArray activity, JArray rawActivity, string key, string? keyId = null)
2422 {
2523 if (activity.ToArray() is not [JObject obj])
···3230 public static async Task<bool> VerifyAsync(JObject activity, JObject rawActivity, string key, string? keyId = null)
3331 {
3432 // ReSharper disable once ExplicitCallerInfoArgument
3535- using var traceActivity = ActivitySource.StartActivity("LD Signature Verify");
3333+ using var traceActivity = Telemetry.ActivitySource.StartActivity("LD Signature Verify");
3634 traceActivity?.AddTag("key", keyId);
37353836 var options = activity[$"{Constants.W3IdSecurityNs}#signature"];
···6462 public static async Task<JObject> SignAsync(JObject activity, string key, string? creator)
6563 {
6664 // ReSharper disable once ExplicitCallerInfoArgument
6767- using var _ = ActivitySource.StartActivity("LD Sign");
6565+ using var _ = Telemetry.ActivitySource.StartActivity("LD Sign");
68666967 var options = new SignatureOptions
7068 {
···142140 [JC(typeof(VC))]
143141 public DateTime? Created { get; set; }
144142 }
145145-}143143+}
+25
Iceshrimp.Backend/Core/Helpers/Telemetry.cs
···11+using System.Diagnostics;
22+using System.Diagnostics.Metrics;
33+using Iceshrimp.Shared.Helpers;
44+55+namespace Iceshrimp.Backend.Core.Helpers;
66+77+/// <summary>
88+/// https://opentelemetry.io/docs/languages/dotnet/instrumentation/#setting-up-an-activitysource
99+///
1010+/// It is recommended to use a custom type to hold references for ActivitySource.
1111+/// This avoids possible type collisions with other components in the DI container.
1212+/// </summary>
1313+public static class Telemetry
1414+{
1515+ public const string Source = "Iceshrimp.NET";
1616+1717+ public static ActivitySource ActivitySource { get; } = new(Source, VersionHelpers.VersionInfo.Value.Version);
1818+ public static Meter Meter { get; } = new(Source, VersionHelpers.VersionInfo.Value.Version);
1919+2020+ public static void Dispose()
2121+ {
2222+ ActivitySource.Dispose();
2323+ Meter.Dispose();
2424+ }
2525+}
+30-12
Iceshrimp.Backend/Core/Services/QueueService.cs
···11using System.Diagnostics;
22using System.Diagnostics.CodeAnalysis;
33+using System.Diagnostics.Metrics;
34using System.Text.Json;
45using Iceshrimp.Backend.Core.Configuration;
56using Iceshrimp.Backend.Core.Database;
···3334 public readonly BackfillUserQueue BackfillUserQueue = new(queueConcurrency.Value.BackfillUser);
34353536 public IEnumerable<string> QueueNames => _queues.Select(p => p.Name);
3737+3838+ protected internal static readonly Counter<long> DelayedJobCounter =
3939+ Telemetry.Meter.CreateCounter<long>("queue.jobs.delayed", "jobs", "number of jobs ever delayed (includes the same job if delayed multiple times)");
4040+4141+ protected internal static readonly Counter<long> FinishedJobCounter =
4242+ Telemetry.Meter.CreateCounter<long>("queue.jobs.finished", "jobs", "number of jobs ever finished");
36433744 protected override async Task ExecuteAsync(CancellationToken token)
3845 {
···209216 private readonly string _exceptionPrefix =
210217 $" at {typeof(PostgresJobQueue<T>).Namespace}.{typeof(PostgresJobQueue<T>).Name}";
211218219219+ private readonly Gauge<long> _queuedJobGauge =
220220+ Telemetry.Meter.CreateGauge<long>($"queue.jobs.queued.{name.Replace('-', '_')}", "jobs", "number of jobs currently queued");
221221+222222+ private readonly Gauge<long> _activeJobGauge =
223223+ Telemetry.Meter.CreateGauge<long>($"queue.jobs.active.{name.Replace('-', '_')}", "jobs", "number of jobs currently active");
224224+212225 /*
213226 * This is the main queue processor loop. The algorithm aims to only ever have as many workers running as needed,
214227 * conserving resources.
···271284 await using var scope = GetScope();
272285 await using var db = GetDbContext(scope);
273286274274- var queuedCount = await db.GetJobQueuedCountAsync(name, token);
287287+ var queuedCount = await db.GetJobQueuedCountAsync(name, token);
288288+ _queuedJobGauge.Record(queuedCount);
289289+ _activeJobGauge.Record(_semaphore.ActiveCount);
290290+275291 var actualParallelism = Math.Min(_semaphore.CurrentCount, queuedCount);
276276-277292 if (actualParallelism == 0)
278293 {
279294 // Not doing this causes a TOC/TOU race condition, even if it'd likely only be a couple CPU cycles wide.
···432447433448 private async Task ProcessJobAsync(IServiceScope processorScope, IServiceScope jobScope, CancellationToken token)
434449 {
435435- var activitySource = processorScope.ServiceProvider.GetRequiredService<TraceService>().ActivitySource;
436436- using var activity = activitySource.StartActivity($"process {name}", ActivityKind.Consumer);
450450+ using var activity = Telemetry.ActivitySource.StartActivity($"process {name}", ActivityKind.Consumer);
437451438452 await using var db = GetDbContext(processorScope);
439453···463477 await db.SaveChangesAsync(token);
464478 activity?.SetStatus(ActivityStatusCode.Error);
465479 activity?.AddEvent(new ActivityEvent(job.ExceptionMessage));
480480+ QueueService.FinishedJobCounter.Add(1, new("messaging.destination.name", name), new("error", true));
466481 return;
467482 }
468483···479494 job.StackTrace = e.StackTrace;
480495 job.Exception = e.ToString();
481496482482- activity?.SetStatus(ActivityStatusCode.Error)
483483- .AddException(e);
497497+ activity?.AddException(e);
484498485499 var queueName = data is BackgroundTaskJobData ? name + $" ({data.GetType().Name})" : name;
486500 if (e is GracefulException { Details: not null } ce)
···504518 if (job.Status is Job.JobStatus.Failed)
505519 {
506520 job.FinishedAt = DateTime.UtcNow;
521521+522522+ activity?.SetStatus(ActivityStatusCode.Error);
523523+ QueueService.FinishedJobCounter.Add(1, new("messaging.destination.name", name), new("error", true));
507524 }
508525 else if (job.Status is Job.JobStatus.Delayed)
509526 {
···519536 _logger.LogTrace("Job in queue {queue} was delayed to {newTime} after {duration} ms, has been queued since {origTime}",
520537 name, job.DelayedUntil.Value.ToLocalTime().ToStringIso8601Like(), job.Duration,
521538 job.QueuedAt.ToLocalTime().ToStringIso8601Like());
539539+ QueueService.DelayedJobCounter.Add(1, [new("messaging.destination.name", name)]);
522540 db.ChangeTracker.Clear();
523541 db.Update(job);
524542 await db.SaveChangesAsync(token);
···530548 {
531549 job.Status = Job.JobStatus.Completed;
532550 job.FinishedAt = DateTime.UtcNow;
551551+552552+ QueueService.FinishedJobCounter.Add(1, new("messaging.destination.name", name), new("error", false));
533553534554 if (job.QueueDuration < 10_000)
535555 {
···550570551571 public async Task EnqueueAsync(T jobData, string? mutex = null)
552572 {
573573+ using var activity = Telemetry.ActivitySource.StartActivity($"schedule {name}", ActivityKind.Producer);
574574+553575 await using var scope = GetScope();
554576 await using var db = GetDbContext(scope);
555555-556556- var activitySource = scope.ServiceProvider.GetRequiredService<TraceService>().ActivitySource;
557557- using var activity = activitySource.StartActivity($"schedule {name}", ActivityKind.Producer);
558577559578 var id = Ulid.NewUlid().ToGuid();
560579···580599581600 public async Task ScheduleAsync(T jobData, DateTime triggerAt, string? mutex = null)
582601 {
602602+ using var activity = Telemetry.ActivitySource.StartActivity($"schedule {name}", ActivityKind.Producer);
603603+583604 await using var scope = GetScope();
584605 await using var db = GetDbContext(scope);
585585-586586- var activitySource = scope.ServiceProvider.GetRequiredService<TraceService>().ActivitySource;
587587- using var activity = activitySource.StartActivity($"schedule {name}", ActivityKind.Producer);
588606589607 var id = Ulid.NewUlid().ToGuid();
590608
-23
Iceshrimp.Backend/Core/Services/TraceService.cs
···11-using System.Diagnostics;
22-using Iceshrimp.Backend.Core.Extensions;
33-using Iceshrimp.Shared.Helpers;
44-55-namespace Iceshrimp.Backend.Core.Services;
66-77-/// <summary>
88-/// https://opentelemetry.io/docs/languages/dotnet/instrumentation/#setting-up-an-activitysource
99-///
1010-/// It is recommended to use a custom type to hold references for ActivitySource.
1111-/// This avoids possible type collisions with other components in the DI container.
1212-/// </summary>
1313-public class TraceService : IDisposable, ISingletonService
1414-{
1515- public const string Source = "Iceshrimp.NET";
1616-1717- public ActivitySource ActivitySource { get; } = new(Source, VersionHelpers.VersionInfo.Value.Version);
1818-1919- public void Dispose()
2020- {
2121- ActivitySource.Dispose();
2222- }
2323-}