Skip to content

Commit 9bd9948

Browse files
pmachapmanmarksvc
authored andcommitted
SF-3622 Definitively associate draft generation metrics
Records SF request id to help find related events. Drafting event metrics that are associated with the same NMT draft generation request have a 'tags.draftGenerationRequestId' written to them with an SF-specific id. This id is transferred around by Activity.Current.Tags, by method arguments, and looked up by finding a BuildProjectAsync event with a Serval build id. When SMT is used, `null` is passed for draftGenerationRequestId method arguments.
1 parent 55ac4b7 commit 9bd9948

File tree

11 files changed

+705
-93
lines changed

11 files changed

+705
-93
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metric.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface EventMetric {
99
timeStamp: string;
1010
userId?: string;
1111
executionTime?: string;
12+
tags?: { [key: string]: any | undefined };
1213
}
1314

1415
export enum EventScope {

src/SIL.XForge.Scripture/Services/IMachineProjectService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ Task BuildProjectForBackgroundJobAsync(
1515
string curUserId,
1616
BuildConfig buildConfig,
1717
bool preTranslate,
18-
CancellationToken cancellationToken
18+
string? draftGenerationRequestId = null,
19+
CancellationToken cancellationToken = default
1920
);
2021
Task<string> GetProjectZipAsync(string sfProjectId, Stream outputStream, CancellationToken cancellationToken);
2122
Task RemoveProjectAsync(string sfProjectId, bool preTranslate, CancellationToken cancellationToken);

src/SIL.XForge.Scripture/Services/MachineApiService.cs

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
45
using System.Security.Cryptography;
56
using System.Text;
@@ -584,6 +585,12 @@ public async Task BuildCompletedAsync(string sfProjectId, string buildId, string
584585
{
585586
try
586587
{
588+
string? draftGenerationRequestId = await GetDraftGenerationRequestIdForBuildAsync(buildId);
589+
if (!string.IsNullOrEmpty(draftGenerationRequestId))
590+
{
591+
Activity.Current?.AddTag("draftGenerationRequestId", draftGenerationRequestId);
592+
}
593+
587594
// Retrieve the build started from the event metric. We do this as there may be multiple builds started,
588595
// and this ensures that only builds that want to send an email will have one sent.
589596
var eventMetrics = await eventMetricService.GetEventMetricsAsync(
@@ -680,8 +687,15 @@ await projectSecrets.UpdateAsync(
680687
cancellationToken
681688
);
682689

690+
string buildId = translationBuild.Id;
691+
string? draftGenerationRequestId = await GetDraftGenerationRequestIdForBuildAsync(buildId);
692+
if (!string.IsNullOrEmpty(draftGenerationRequestId))
693+
{
694+
Activity.Current?.AddTag("draftGenerationRequestId", draftGenerationRequestId);
695+
}
696+
683697
// Return the build id so it can be logged
684-
return translationBuild.Id;
698+
return buildId;
685699
}
686700
catch (ServalApiException e) when (e.StatusCode == StatusCodes.Status404NotFound)
687701
{
@@ -765,6 +779,13 @@ public async Task ExecuteWebhookAsync(string json, string signature)
765779
return;
766780
}
767781

782+
// Add the draftGenerationRequestId to associate with other events.
783+
string? draftGenerationRequestId = await GetDraftGenerationRequestIdForBuildAsync(buildId);
784+
if (!string.IsNullOrEmpty(draftGenerationRequestId))
785+
{
786+
Activity.Current?.AddTag("draftGenerationRequestId", draftGenerationRequestId);
787+
}
788+
768789
// Record that the webhook was run successfully
769790
var arguments = new Dictionary<string, object>
770791
{
@@ -1800,17 +1821,23 @@ await projectSecrets.UpdateAsync(
18001821
);
18011822

18021823
// Notify any SignalR clients subscribed to the project
1824+
string? buildId = translationBuild?.Id;
18031825
await hubContext.NotifyBuildProgress(
18041826
sfProjectId,
1805-
new ServalBuildState
1827+
new ServalBuildState { BuildId = buildId, State = nameof(ServalData.PreTranslationsRetrieved) }
1828+
);
1829+
1830+
if (!string.IsNullOrEmpty(buildId))
1831+
{
1832+
string? draftGenerationRequestId = await GetDraftGenerationRequestIdForBuildAsync(buildId);
1833+
if (!string.IsNullOrEmpty(draftGenerationRequestId))
18061834
{
1807-
BuildId = translationBuild?.Id,
1808-
State = nameof(ServalData.PreTranslationsRetrieved),
1835+
Activity.Current?.AddTag("draftGenerationRequestId", draftGenerationRequestId);
18091836
}
1810-
);
1837+
}
18111838

18121839
// Return the build id
1813-
return translationBuild?.Id;
1840+
return buildId;
18141841
}
18151842
}
18161843
catch (TaskCanceledException e) when (e.InnerException is not TimeoutException)
@@ -1879,6 +1906,7 @@ public async Task StartBuildAsync(string curUserId, string sfProjectId, Cancella
18791906
curUserId,
18801907
new BuildConfig { ProjectId = sfProjectId },
18811908
false,
1909+
null,
18821910
CancellationToken.None
18831911
),
18841912
null,
@@ -1904,6 +1932,8 @@ public async Task StartPreTranslationBuildAsync(
19041932
CancellationToken cancellationToken
19051933
)
19061934
{
1935+
string draftGenerationRequestId = ObjectId.GenerateNewId().ToString();
1936+
Activity.Current?.AddTag("draftGenerationRequestId", draftGenerationRequestId);
19071937
// Load the project from the realtime service
19081938
await using IConnection conn = await realtimeService.ConnectAsync(curUserId);
19091939
IDocument<SFProject> projectDoc = await conn.FetchAsync<SFProject>(buildConfig.ProjectId);
@@ -2018,7 +2048,14 @@ await projectDoc.SubmitJson0OpAsync(op =>
20182048
// so that the interceptor functions for BuildProjectAsync().
20192049
jobId = backgroundJobClient.ContinueJobWith<MachineProjectService>(
20202050
jobId,
2021-
r => r.BuildProjectForBackgroundJobAsync(curUserId, buildConfig, true, CancellationToken.None)
2051+
r =>
2052+
r.BuildProjectForBackgroundJobAsync(
2053+
curUserId,
2054+
buildConfig,
2055+
true,
2056+
draftGenerationRequestId,
2057+
CancellationToken.None
2058+
)
20222059
);
20232060

20242061
// Set the pre-translation queued date and time, and hang fire job id
@@ -2586,6 +2623,29 @@ CancellationToken cancellationToken
25862623
return project;
25872624
}
25882625

2626+
/// <summary>
2627+
/// Gets the SF-specific draft generation request identifier for a build by looking up the BuildProjectAsync event.
2628+
/// </summary>
2629+
/// <param name="buildId">The Serval build identifier.</param>
2630+
/// <returns>The draft generation request identifier, or null if not found.</returns>
2631+
private async Task<string?> GetDraftGenerationRequestIdForBuildAsync(string buildId)
2632+
{
2633+
// BuildProjectAsync events serve as a record of what Serval build id corresponds to what draft generation
2634+
// request id.
2635+
const int lookupTimeframeDays = 60;
2636+
DateTime startDate = DateTime.UtcNow.AddDays(-lookupTimeframeDays);
2637+
QueryResults<EventMetric> buildProjectEvents = await eventMetricService.GetEventMetricsAsync(
2638+
projectId: null,
2639+
scopes: [EventScope.Drafting],
2640+
eventTypes: [nameof(MachineProjectService.BuildProjectAsync)],
2641+
fromDate: startDate
2642+
);
2643+
EventMetric? buildEvent = buildProjectEvents.Results.FirstOrDefault(e => e.Result?.ToString() == buildId);
2644+
return (buildEvent?.Tags?.TryGetValue("draftGenerationRequestId", out BsonValue? requestId) == true)
2645+
? requestId?.AsString
2646+
: null;
2647+
}
2648+
25892649
private async Task<string> GetTranslationIdAsync(
25902650
string sfProjectId,
25912651
bool preTranslate,

src/SIL.XForge.Scripture/Services/MachineProjectService.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ public async Task<string> AddSmtProjectAsync(string sfProjectId, CancellationTok
106106
/// <param name="curUserId">The current user identifier.</param>
107107
/// <param name="buildConfig">The build configuration.</param>
108108
/// <param name="preTranslate">If <c>true</c> use NMT; otherwise if <c>false</c> use SMT.</param>
109+
/// <param name="draftGenerationRequestId">
110+
/// The draft generation request identifier (NMT only). Pass null for SMT builds.
111+
/// </param>
109112
/// <param name="cancellationToken">The cancellation token.</param>
110113
/// <returns>An asynchronous task.</returns>
111114
/// <remarks>
@@ -115,12 +118,18 @@ public async Task BuildProjectForBackgroundJobAsync(
115118
string curUserId,
116119
BuildConfig buildConfig,
117120
bool preTranslate,
118-
CancellationToken cancellationToken
121+
string? draftGenerationRequestId = null,
122+
CancellationToken cancellationToken = default
119123
)
120124
{
125+
if (!string.IsNullOrEmpty(draftGenerationRequestId))
126+
{
127+
System.Diagnostics.Activity.Current?.AddTag("draftGenerationRequestId", draftGenerationRequestId);
128+
}
129+
121130
try
122131
{
123-
await BuildProjectAsync(curUserId, buildConfig, preTranslate, cancellationToken);
132+
await BuildProjectAsync(curUserId, buildConfig, preTranslate, draftGenerationRequestId, cancellationToken);
124133
}
125134
catch (TaskCanceledException e) when (e.InnerException is not TimeoutException)
126135
{
@@ -598,8 +607,11 @@ await projectDoc.SubmitJson0OpAsync(op =>
598607
/// <param name="curUserId">The current user identifier.</param>
599608
/// <param name="buildConfig">The build configuration.</param>
600609
/// <param name="preTranslate">If <c>true</c> use NMT; otherwise if <c>false</c> use SMT.</param>
610+
/// <param name="draftGenerationRequestId">
611+
/// The draft generation request identifier (NMT only). Pass null for SMT builds.
612+
/// </param>
601613
/// <param name="cancellationToken">The cancellation token.</param>
602-
/// <returns>An asynchronous task.</returns>
614+
/// <returns>Serval build ID</returns>
603615
/// <exception cref="DataNotFoundException">The project or project secret could not be found.</exception>
604616
/// <exception cref="InvalidDataException">The language of the source project was not specified.</exception>
605617
/// <remarks>
@@ -616,9 +628,15 @@ public virtual async Task<string> BuildProjectAsync(
616628
string curUserId,
617629
BuildConfig buildConfig,
618630
bool preTranslate,
619-
CancellationToken cancellationToken
631+
string? draftGenerationRequestId = null,
632+
CancellationToken cancellationToken = default
620633
)
621634
{
635+
if (!string.IsNullOrEmpty(draftGenerationRequestId))
636+
{
637+
System.Diagnostics.Activity.Current?.AddTag("draftGenerationRequestId", draftGenerationRequestId);
638+
}
639+
622640
// Load the target project secrets, so we can get the translation engine ID
623641
if (
624642
!(await projectSecrets.TryGetAsync(buildConfig.ProjectId, cancellationToken)).TryResult(

src/SIL.XForge.Scripture/Services/SyncService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ await projectSecrets.UpdateAsync(
198198
syncConfig.UserId,
199199
new BuildConfig { ProjectId = syncConfig.ProjectId },
200200
false,
201+
null,
201202
CancellationToken.None
202203
),
203204
null,

src/SIL.XForge/EventMetrics/EventMetric.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class EventMetric : IIdentifiable
3636
public string Id { get; set; } = string.Empty;
3737

3838
/// <summary>
39-
/// Gets or sets the event payload.
39+
/// Gets or sets the event payload, which contains the arguments given to the RPC or method call being recorded.
4040
/// </summary>
4141
/// <remarks>
4242
/// <para>If you are querying by projectId or userId, that will be done here.</para>
@@ -73,4 +73,12 @@ public class EventMetric : IIdentifiable
7373
/// Gets or sets the event user identifier.
7474
/// </summary>
7575
public string? UserId { get; set; }
76+
77+
/// <summary>
78+
/// Additional event metadata. For example, from items in an Activity Tags.
79+
/// </summary>
80+
/// <remarks>
81+
/// <para>Keys should be normalized to lowerCamelCase.</para>
82+
/// </remarks>
83+
public Dictionary<string, BsonValue?>? Tags { get; set; }
7684
}

src/SIL.XForge/EventMetrics/EventMetricLogger.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public void Intercept(IInvocation invocation)
5656
is LogEventMetricAttribute logEventMetricAttribute
5757
)
5858
{
59+
// Start an activity so additional information can be logged via tags
60+
Activity activity = new Activity("log_event_metric").Start();
61+
5962
// Invoke the method, then record its event metrics
6063
Task task;
6164
Stopwatch stopwatch = Stopwatch.StartNew();
@@ -68,21 +71,21 @@ is LogEventMetricAttribute logEventMetricAttribute
6871
task = methodTask.ContinueWith(t =>
6972
{
7073
stopwatch.Stop();
71-
return SaveEventMetricAsync(stopwatch.Elapsed, t.Exception);
74+
return SaveEventMetricAsync(stopwatch.Elapsed, activity, t.Exception);
7275
});
7376
}
7477
else
7578
{
7679
// Save the event metric in another thread after the method has executed
7780
stopwatch.Stop();
78-
task = Task.Run(() => SaveEventMetricAsync(stopwatch.Elapsed));
81+
task = Task.Run(() => SaveEventMetricAsync(stopwatch.Elapsed, activity));
7982
}
8083
}
8184
catch (Exception e)
8285
{
8386
// Save the error in the event metric, as the Proceed() will have faulted
8487
stopwatch.Stop();
85-
task = Task.Run(() => SaveEventMetricAsync(stopwatch.Elapsed, e));
88+
task = Task.Run(() => SaveEventMetricAsync(stopwatch.Elapsed, activity, e));
8689

8790
// Notify observers of the task of immediate completion
8891
TaskStarted?.Invoke(task);
@@ -97,7 +100,7 @@ is LogEventMetricAttribute logEventMetricAttribute
97100

98101
// Run as a separate task so we do not slow down the method execution
99102
// Unless we want the return value, in which case we will not write the metric until the method returns
100-
async Task SaveEventMetricAsync(TimeSpan executionTime, Exception? exception = null)
103+
async Task SaveEventMetricAsync(TimeSpan executionTime, Activity activity, Exception? exception = null)
101104
{
102105
string methodName = invocation.Method.Name;
103106
try
@@ -194,6 +197,10 @@ await eventMetricService.SaveEventMetricAsync(
194197
// Just log any errors rather than throwing
195198
logger.LogError(e, "Error logging event metric for {methodName}", methodName);
196199
}
200+
finally
201+
{
202+
activity.Dispose();
203+
}
197204
}
198205
}
199206

src/SIL.XForge/Services/EventMetricService.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
45
using System.Threading;
56
using System.Threading.Tasks;
@@ -93,6 +94,27 @@ public async Task SaveEventMetricAsync(
9394
payload[kvp.Key] = GetBsonValue(kvp.Value);
9495
}
9596

97+
// Collect tags from Activity.Current and all parent activities
98+
// Child activity tags override parent tags with the same key
99+
Dictionary<string, BsonValue?>? tags = null;
100+
var collectedTags = new Dictionary<string, string?>();
101+
102+
// Walk up the activity chain collecting tags (child first, so child overrides parent)
103+
var activity = Activity.Current;
104+
while (activity is not null)
105+
{
106+
collectedTags = activity
107+
.Tags.Where(kvp => !collectedTags.ContainsKey(kvp.Key))
108+
.Union(collectedTags)
109+
.ToDictionary();
110+
activity = activity.Parent;
111+
}
112+
113+
if (collectedTags.Count > 0)
114+
{
115+
tags = collectedTags.ToDictionary(kvp => kvp.Key, kvp => GetBsonValue(kvp.Value));
116+
}
117+
96118
// Generate the event metric
97119
var eventMetric = new EventMetric
98120
{
@@ -103,6 +125,7 @@ public async Task SaveEventMetricAsync(
103125
ProjectId = projectId,
104126
Scope = scope,
105127
UserId = userId,
128+
Tags = tags,
106129
};
107130

108131
// Do not set Result if it is null, or the document will contain "result: null"

0 commit comments

Comments
 (0)