Skip to content

Commit 537657a

Browse files
authored
Merge pull request #10 from pkgdiff-dev/gsd/quick-261020-job-labels
feat(admin): show job type, target and trigger in the jobs panel
2 parents 909791c + a9c0095 commit 537657a

8 files changed

Lines changed: 285 additions & 56 deletions

File tree

‎src/backend/PkgDiff.API/Extensions/HostingServiceCollectionExtensions.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.AspNetCore.DataProtection;
55
using Microsoft.AspNetCore.Identity;
66
using Microsoft.EntityFrameworkCore;
7+
using PkgDiff.API.Filters;
78
using PkgDiff.API.Jobs;
89
using PkgDiff.API.Middleware;
910
using PkgDiff.API.Options;
@@ -84,10 +85,12 @@ public static IServiceCollection AddPkgDiffPersistence(this IServiceCollection s
8485

8586
public static IServiceCollection AddPkgDiffBackgroundJobs(this IServiceCollection services, IConfiguration configuration)
8687
{
87-
services.AddHangfire(config => config
88+
services.AddHttpContextAccessor();
89+
services.AddHangfire((provider, config) => config
8890
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
8991
.UseSimpleAssemblyNameTypeSerializer()
9092
.UseRecommendedSerializerSettings()
93+
.UseFilter(new JobTriggerFilter(provider.GetRequiredService<IHttpContextAccessor>()))
9194
.UsePostgreSqlStorage(options =>
9295
options.UseNpgsqlConnection(configuration.GetConnectionString("DefaultConnection"))));
9396

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Hangfire.Client;
2+
using Hangfire.Server;
3+
using PkgDiff.Application.Jobs;
4+
5+
namespace PkgDiff.API.Filters;
6+
7+
public class JobTriggerFilter : IClientFilter, IServerFilter
8+
{
9+
private static readonly AsyncLocal<string?> CurrentJob = new();
10+
private readonly IHttpContextAccessor _httpContextAccessor;
11+
12+
public JobTriggerFilter(IHttpContextAccessor httpContextAccessor)
13+
{
14+
_httpContextAccessor = httpContextAccessor;
15+
}
16+
17+
public void OnCreating(CreatingContext context)
18+
{
19+
var trigger = CurrentJob.Value ?? DescribeRequest();
20+
if (trigger != null)
21+
context.SetJobParameter(JobDescriptions.TriggerParameter, trigger);
22+
}
23+
24+
public void OnCreated(CreatedContext context)
25+
{
26+
}
27+
28+
public void OnPerforming(PerformingContext context)
29+
{
30+
CurrentJob.Value = $"Job #{context.BackgroundJob.Id} · {JobDescriptions.Label(context.BackgroundJob.Job)}";
31+
}
32+
33+
public void OnPerformed(PerformedContext context)
34+
{
35+
CurrentJob.Value = null;
36+
}
37+
38+
private string? DescribeRequest()
39+
{
40+
try
41+
{
42+
var request = _httpContextAccessor.HttpContext?.Request;
43+
return request == null ? null : $"Request · {request.Method} {request.Path}";
44+
}
45+
catch (ObjectDisposedException)
46+
{
47+
return null;
48+
}
49+
}
50+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Text.RegularExpressions;
2+
using Hangfire.Common;
3+
using PkgDiff.Shared.DTOs;
4+
5+
namespace PkgDiff.Application.Jobs;
6+
7+
public static class JobDescriptions
8+
{
9+
public const string TriggerParameter = "TriggeredBy";
10+
11+
private static readonly Dictionary<string, string> Labels = new()
12+
{
13+
["PackageAnalysisJob.ExecuteAsync"] = "Cache refresh sweep",
14+
["PackageRefreshJob.RefreshAsync"] = "Package cache refresh",
15+
["IPackageTrustService.RefreshAsync"] = "Security scan",
16+
["IPackageTrustService.RefreshExpiredAsync"] = "Expired security re-scan",
17+
["IPackageCacheService.GetOrFetchMetadataAsync"] = "Metadata refresh",
18+
["IPackageAnalysisService.ScrapePackageReleaseNotesAsync"] = "Release notes scrape",
19+
["IDependencyAuditService.RunAsync"] = "Dependency audit",
20+
["IDependencyAuditService.DeleteExpiredAsync"] = "Audit cleanup",
21+
["DatabaseCleanupJob.CleanupOldRequestLogsAsync"] = "Request log cleanup",
22+
["IAutoPilotService.RunAutoPilotJobAsync"] = "AutoPilot blog generation",
23+
["BlogSocialPostJob.ExecuteAsync"] = "Blog social post",
24+
["IContributorService.BackfillFromExistingReleaseNotesAsync"] = "Contributor backfill"
25+
};
26+
27+
public static string TechnicalName(Job? job) =>
28+
job == null ? "Unknown" : $"{job.Type.Name}.{job.Method.Name}";
29+
30+
public static string Label(Job? job)
31+
{
32+
if (job == null) return "Unknown job";
33+
return Labels.TryGetValue(TechnicalName(job), out var label) ? label : Humanize(job.Method.Name);
34+
}
35+
36+
public static JobTarget Target(Job? job)
37+
{
38+
var target = new JobTarget();
39+
if (job?.Args == null) return target;
40+
41+
var parameters = job.Method.GetParameters();
42+
var details = new List<string>();
43+
44+
for (var i = 0; i < parameters.Length && i < job.Args.Count; i++)
45+
{
46+
var arg = job.Args[i];
47+
switch (arg)
48+
{
49+
case null or bool:
50+
break;
51+
case AnalyzeRequest request:
52+
target.Registry = request.Registry;
53+
target.PackageId = request.PackageId;
54+
if (!string.IsNullOrEmpty(request.CurrentVersion) || !string.IsNullOrEmpty(request.TargetVersion))
55+
details.Add($"{request.CurrentVersion} → {request.TargetVersion}");
56+
break;
57+
case Guid id:
58+
details.Add(id.ToString("N")[..8]);
59+
break;
60+
default:
61+
var value = arg.ToString();
62+
if (string.IsNullOrWhiteSpace(value)) break;
63+
if (parameters[i].Name == "registry") target.Registry = value;
64+
else if (parameters[i].Name == "packageId") target.PackageId = value;
65+
else details.Add($"{Humanize(parameters[i].Name ?? string.Empty)} {value}");
66+
break;
67+
}
68+
}
69+
70+
target.Detail = details.Count > 0 ? string.Join(", ", details) : null;
71+
return target;
72+
}
73+
74+
private static string Humanize(string name)
75+
{
76+
var trimmed = name.EndsWith("Async") ? name[..^5] : name;
77+
var spaced = Regex.Replace(trimmed, "(?<=[a-z0-9])(?=[A-Z])", " ").ToLowerInvariant();
78+
return spaced.Length == 0 ? name : char.ToUpperInvariant(spaced[0]) + spaced[1..];
79+
}
80+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace PkgDiff.Application.Jobs;
2+
3+
public class JobTarget
4+
{
5+
public string? Registry { get; set; }
6+
public string? PackageId { get; set; }
7+
public string? Detail { get; set; }
8+
}

‎src/backend/PkgDiff.Application/Services/AdminJobService.cs‎

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using PkgDiff.Application.Interfaces;
2-
using PkgDiff.Domain.Entities;
3-
using PkgDiff.Domain.Interfaces;
2+
using PkgDiff.Application.Jobs;
43
using PkgDiff.Shared.DTOs;
54
using Hangfire;
65
using Hangfire.Storage;
7-
using Microsoft.AspNetCore.Identity;
6+
using Hangfire.Storage.Monitoring;
87

98
namespace PkgDiff.Application.Services;
109

@@ -35,30 +34,40 @@ public Task<JobsOverview> GetJobsOverviewAsync()
3534
foreach (var job in succeeded)
3635
{
3736
var details = monitor.JobDetails(job.Key);
37+
var target = JobDescriptions.Target(job.Value.Job);
3838
allJobs.Add(new RecentJobInfo
3939
{
4040
JobId = job.Key,
41-
JobName = GetJobDisplayName(job.Value.Job),
41+
JobName = JobDescriptions.TechnicalName(job.Value.Job),
42+
JobType = JobDescriptions.Label(job.Value.Job),
4243
State = "Succeeded",
4344
CreatedAt = details?.CreatedAt,
4445
CompletedAt = job.Value.SucceededAt,
4546
Duration = job.Value.TotalDuration,
46-
PackageId = ExtractPackageId(job.Value.Job)
47+
PackageId = target.PackageId,
48+
Registry = target.Registry,
49+
Detail = target.Detail,
50+
TriggeredBy = DescribeTrigger(details)
4751
});
4852
}
4953

5054
foreach (var job in failed)
5155
{
5256
var details = monitor.JobDetails(job.Key);
57+
var target = JobDescriptions.Target(job.Value.Job);
5358
allJobs.Add(new RecentJobInfo
5459
{
5560
JobId = job.Key,
56-
JobName = GetJobDisplayName(job.Value.Job),
61+
JobName = JobDescriptions.TechnicalName(job.Value.Job),
62+
JobType = JobDescriptions.Label(job.Value.Job),
5763
State = "Failed",
5864
CreatedAt = details?.CreatedAt,
5965
CompletedAt = job.Value.FailedAt,
6066
ExceptionMessage = job.Value.ExceptionMessage,
61-
PackageId = ExtractPackageId(job.Value.Job)
67+
PackageId = target.PackageId,
68+
Registry = target.Registry,
69+
Detail = target.Detail,
70+
TriggeredBy = DescribeTrigger(details)
6271
});
6372
}
6473

@@ -104,28 +113,38 @@ public Task<JobsOverview> GetJobsOverviewAsync()
104113
var enqueuedJobs = monitor.EnqueuedJobs(queue.Name, 0, 50);
105114
foreach (var job in enqueuedJobs)
106115
{
116+
var target = JobDescriptions.Target(job.Value.Job);
107117
queuedJobs.Add(new QueuedJobInfo
108118
{
109119
JobId = job.Key,
110-
JobName = GetJobDisplayName(job.Value.Job),
120+
JobName = JobDescriptions.TechnicalName(job.Value.Job),
121+
JobType = JobDescriptions.Label(job.Value.Job),
111122
State = "Enqueued",
112123
EnqueuedAt = job.Value.EnqueuedAt,
113-
PackageId = ExtractPackageId(job.Value.Job)
124+
PackageId = target.PackageId,
125+
Registry = target.Registry,
126+
Detail = target.Detail,
127+
TriggeredBy = DescribeTrigger(monitor.JobDetails(job.Key))
114128
});
115129
}
116130
}
117131

118132
var processingJobs = monitor.ProcessingJobs(0, 50);
119133
foreach (var job in processingJobs)
120134
{
135+
var target = JobDescriptions.Target(job.Value.Job);
121136
queuedJobs.Add(new QueuedJobInfo
122137
{
123138
JobId = job.Key,
124-
JobName = GetJobDisplayName(job.Value.Job),
139+
JobName = JobDescriptions.TechnicalName(job.Value.Job),
140+
JobType = JobDescriptions.Label(job.Value.Job),
125141
State = "Processing",
126142
StartedAt = job.Value.StartedAt,
127143
ServerId = job.Value.ServerId,
128-
PackageId = ExtractPackageId(job.Value.Job)
144+
PackageId = target.PackageId,
145+
Registry = target.Registry,
146+
Detail = target.Detail,
147+
TriggeredBy = DescribeTrigger(monitor.JobDetails(job.Key))
129148
});
130149
}
131150

@@ -142,35 +161,31 @@ public Task<JobsOverview> GetJobsOverviewAsync()
142161
});
143162
}
144163

145-
private static string GetJobDisplayName(Hangfire.Common.Job? job)
164+
private static string? DescribeTrigger(JobDetailsDto? details)
146165
{
147-
if (job == null) return "Unknown";
148-
return $"{job.Type.Name}.{job.Method.Name}";
166+
if (details?.Properties == null) return null;
167+
168+
if (details.Properties.TryGetValue(JobDescriptions.TriggerParameter, out var trigger))
169+
return ReadParameter(trigger);
170+
171+
if (details.Properties.TryGetValue("RecurringJobId", out var recurringJobId))
172+
return $"Schedule · {ReadParameter(recurringJobId)}";
173+
174+
return null;
149175
}
150176

151-
private static string? ExtractPackageId(Hangfire.Common.Job? job)
177+
private static string? ReadParameter(string? value)
152178
{
153-
if (job?.Args == null || job.Args.Count == 0) return null;
154-
155-
if (job.Args[0] is AnalyzeRequest request)
156-
return request.PackageId;
179+
if (string.IsNullOrEmpty(value)) return null;
157180

158181
try
159182
{
160-
var json = job.Args[0]?.ToString();
161-
if (json != null && json.Contains("PackageId"))
162-
{
163-
using var doc = System.Text.Json.JsonDocument.Parse(json);
164-
if (doc.RootElement.TryGetProperty("PackageId", out var prop))
165-
return prop.GetString();
166-
}
183+
return System.Text.Json.JsonSerializer.Deserialize<string>(value);
167184
}
168185
catch (System.Text.Json.JsonException)
169186
{
170-
// The argument is not JSON: this job has no package to show
187+
return value.Trim('"');
171188
}
172-
173-
return null;
174189
}
175190

176191
public Task TriggerJobAsync(string jobId)

‎src/backend/PkgDiff.Shared/DTOs/AdminModels.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,32 @@ public class RecentJobInfo
129129
{
130130
public string JobId { get; set; } = string.Empty;
131131
public string JobName { get; set; } = string.Empty;
132+
public string JobType { get; set; } = string.Empty;
132133
public string State { get; set; } = string.Empty;
133134
public DateTime? CreatedAt { get; set; }
134135
public DateTime? CompletedAt { get; set; }
135136
public long? Duration { get; set; }
136137
public string? ExceptionMessage { get; set; }
137138
public string? PackageId { get; set; }
139+
public string? Registry { get; set; }
140+
public string? Detail { get; set; }
141+
public string? TriggeredBy { get; set; }
138142
public List<RecentJobInfo> ChildJobs { get; set; } = new();
139143
}
140144

141145
public class QueuedJobInfo
142146
{
143147
public string JobId { get; set; } = string.Empty;
144148
public string JobName { get; set; } = string.Empty;
149+
public string JobType { get; set; } = string.Empty;
145150
public string State { get; set; } = string.Empty;
146151
public DateTime? EnqueuedAt { get; set; }
147152
public DateTime? StartedAt { get; set; }
148153
public string? ServerId { get; set; }
149154
public string? PackageId { get; set; }
155+
public string? Registry { get; set; }
156+
public string? Detail { get; set; }
157+
public string? TriggeredBy { get; set; }
150158
}
151159

152160
public class SystemSettingDto

0 commit comments

Comments
 (0)