Skip to content

Commit e60d532

Browse files
committed
feat(requestlog): persist queue and change metadata
Summary: Request logs now carry queue and change metadata so downstream read models can build queue-scoped views without reloading the original request. Existing SQID parsing remains as a fallback for older or minimal log producers. Test Plan: ✅ `make fmt && make build && make test && make check-mocks && make e2e-test`
1 parent 3a6057c commit e60d532

6 files changed

Lines changed: 158 additions & 5 deletions

File tree

submitqueue/entity/request_log.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package entity
1616

1717
import (
1818
"encoding/json"
19+
"strings"
1920
"time"
2021
)
2122

@@ -96,6 +97,10 @@ const (
9697
type RequestLog struct {
9798
// RequestID is the ID of the request this log entry belongs to. References entity.Request.ID.
9899
RequestID string `json:"request_id"`
100+
// Queue is the queue this request belongs to. New log producers should set it explicitly.
101+
Queue string `json:"queue"`
102+
// ChangeURIs are the original change URIs submitted with the request. They are populated by gateway-originated accepted logs.
103+
ChangeURIs []string `json:"change_uris"`
99104
// TimestampMs is the time this log entry was created, in milliseconds since Unix epoch.
100105
TimestampMs int64 `json:"timestamp_ms"`
101106
// Status is the request status at the time this log entry was created. It may contain requests states from the state machine and also display-friendly intermediate statuses.
@@ -117,11 +122,24 @@ type RequestLog struct {
117122
// lastError is the last error message associated with the status at the time of this log entry, empty string if no error.
118123
// metadata is a set of key-value pairs providing additional context for this log entry. Not constrained to any specific format or schema, used for display or debugging purposes.
119124
func NewRequestLog(requestID string, status RequestStatus, requestVersion int32, lastError string, metadata map[string]string) RequestLog {
125+
return NewRequestLogWithDetails(requestID, QueueFromRequestID(requestID), nil, status, requestVersion, lastError, metadata)
126+
}
127+
128+
// NewRequestLogWithDetails creates a new RequestLog with queue and change information for list summaries.
129+
func NewRequestLogWithDetails(requestID, queue string, changeURIs []string, status RequestStatus, requestVersion int32, lastError string, metadata map[string]string) RequestLog {
120130
if metadata == nil {
121131
metadata = make(map[string]string)
122132
}
133+
if queue == "" {
134+
queue = QueueFromRequestID(requestID)
135+
}
136+
if changeURIs == nil {
137+
changeURIs = []string{}
138+
}
123139
return RequestLog{
124140
RequestID: requestID,
141+
Queue: queue,
142+
ChangeURIs: changeURIs,
125143
TimestampMs: time.Now().UnixMilli(),
126144
Status: status,
127145
RequestVersion: requestVersion,
@@ -146,5 +164,53 @@ func RequestLogFromBytes(data []byte) (RequestLog, error) {
146164
if log.Metadata == nil {
147165
log.Metadata = make(map[string]string)
148166
}
167+
if log.ChangeURIs == nil {
168+
log.ChangeURIs = []string{}
169+
}
170+
if log.Queue == "" {
171+
log.Queue = QueueFromRequestID(log.RequestID)
172+
}
149173
return log, nil
150174
}
175+
176+
// QueueFromRequestID extracts the queue from the current "<queue>/<number>" request ID format.
177+
// It strips only a trailing numeric path segment so queue names may contain slashes.
178+
func QueueFromRequestID(requestID string) string {
179+
idx := strings.LastIndex(requestID, "/")
180+
if idx <= 0 || idx == len(requestID)-1 {
181+
return ""
182+
}
183+
for _, r := range requestID[idx+1:] {
184+
if r < '0' || r > '9' {
185+
return ""
186+
}
187+
}
188+
return requestID[:idx]
189+
}
190+
191+
// IsKnownRequestStatus returns true if status is a public request status emitted by SubmitQueue.
192+
func IsKnownRequestStatus(status RequestStatus) bool {
193+
switch status {
194+
case RequestStatusAccepted,
195+
RequestStatusStarted,
196+
RequestStatusValidating,
197+
RequestStatusValidated,
198+
RequestStatusBatching,
199+
RequestStatusBatched,
200+
RequestStatusScored,
201+
RequestStatusSpeculating,
202+
RequestStatusSpeculated,
203+
RequestStatusBuilding,
204+
RequestStatusBuilt,
205+
RequestStatusWaitingPath,
206+
RequestStatusLanding,
207+
RequestStatusProcessing,
208+
RequestStatusLanded,
209+
RequestStatusError,
210+
RequestStatusCancelling,
211+
RequestStatusCancelled:
212+
return true
213+
default:
214+
return false
215+
}
216+
}

submitqueue/entity/request_log_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func TestRequestLog_ToBytes(t *testing.T) {
5151
func TestRequestLogFromBytes(t *testing.T) {
5252
original := RequestLog{
5353
RequestID: "my-queue/999",
54+
Queue: "my-queue",
55+
ChangeURIs: []string{"github://uber/repo/pull/1/abcdef"},
5456
TimestampMs: 1709568000000,
5557
Status: RequestStatusProcessing,
5658
RequestVersion: 3,
@@ -65,6 +67,8 @@ func TestRequestLogFromBytes(t *testing.T) {
6567
require.NoError(t, err)
6668

6769
assert.Equal(t, original.RequestID, deserialized.RequestID)
70+
assert.Equal(t, original.Queue, deserialized.Queue)
71+
assert.Equal(t, original.ChangeURIs, deserialized.ChangeURIs)
6872
assert.Equal(t, original.TimestampMs, deserialized.TimestampMs)
6973
assert.Equal(t, original.Status, deserialized.Status)
7074
assert.Equal(t, original.RequestVersion, deserialized.RequestVersion)
@@ -103,6 +107,8 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
103107
name: "with all fields populated",
104108
log: RequestLog{
105109
RequestID: "queue1/100",
110+
Queue: "queue1",
111+
ChangeURIs: []string{},
106112
TimestampMs: 1709568000000,
107113
Status: RequestStatusLanded,
108114
RequestVersion: 5,
@@ -114,6 +120,8 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
114120
name: "with error",
115121
log: RequestLog{
116122
RequestID: "queue2/200",
123+
Queue: "queue2",
124+
ChangeURIs: []string{},
117125
TimestampMs: 1709568001000,
118126
Status: RequestStatusError,
119127
RequestVersion: 2,
@@ -125,6 +133,8 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
125133
name: "with zero version",
126134
log: RequestLog{
127135
RequestID: "queue3/300",
136+
Queue: "queue3",
137+
ChangeURIs: []string{},
128138
TimestampMs: 1709568002000,
129139
Status: RequestStatusStarted,
130140
RequestVersion: 0,
@@ -146,3 +156,10 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
146156
})
147157
}
148158
}
159+
160+
func TestQueueFromRequestID(t *testing.T) {
161+
assert.Equal(t, "queue", QueueFromRequestID("queue/100"))
162+
assert.Equal(t, "org/queue", QueueFromRequestID("org/queue/100"))
163+
assert.Empty(t, QueueFromRequestID("queue/not-a-number"))
164+
assert.Empty(t, QueueFromRequestID("queue"))
165+
}

submitqueue/extension/storage/mysql/BUILD.bazel

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@rules_go//go:def.bzl", "go_library")
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "mysql",
@@ -22,3 +22,14 @@ go_library(
2222
"@com_github_uber_go_tally//:tally",
2323
],
2424
)
25+
26+
go_test(
27+
name = "mysql_test",
28+
srcs = ["request_log_store_test.go"],
29+
embed = [":mysql"],
30+
deps = [
31+
"//submitqueue/entity",
32+
"@com_github_stretchr_testify//require",
33+
"@com_github_uber_go_tally//:tally",
34+
],
35+
)

submitqueue/extension/storage/mysql/request_log_store.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ func (r *requestLogStore) Insert(ctx context.Context, log entity.RequestLog) (re
4646
op := metrics.Begin(r.scope, "insert")
4747
defer func() { op.Complete(retErr) }()
4848

49+
if log.Queue == "" {
50+
log.Queue = entity.QueueFromRequestID(log.RequestID)
51+
}
52+
if log.Queue == "" {
53+
return fmt.Errorf("request log insert requires queue for request_id=%s", log.RequestID)
54+
}
55+
if log.ChangeURIs == nil {
56+
log.ChangeURIs = []string{}
57+
}
58+
59+
changeURIsJSON, err := json.Marshal(log.ChangeURIs)
60+
if err != nil {
61+
return fmt.Errorf("failed to marshal change URIs for request log request_id=%s: %w", log.RequestID, err)
62+
}
63+
4964
metadataJSON, err := json.Marshal(log.Metadata)
5065
if err != nil {
5166
return fmt.Errorf("failed to marshal metadata for request log request_id=%s: %w", log.RequestID, err)
@@ -57,8 +72,8 @@ func (r *requestLogStore) Insert(ctx context.Context, log entity.RequestLog) (re
5772
salt := rand.Int64()
5873

5974
_, err = r.db.ExecContext(ctx,
60-
"INSERT INTO request_log (request_id, timestamp_ms, salt, status, request_version, last_error, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
61-
log.RequestID, log.TimestampMs, salt, log.Status, log.RequestVersion, log.LastError, metadataJSON,
75+
"INSERT INTO request_log (request_id, queue, change_uri, timestamp_ms, salt, status, request_version, last_error, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
76+
log.RequestID, log.Queue, changeURIsJSON, log.TimestampMs, salt, log.Status, log.RequestVersion, log.LastError, metadataJSON,
6277
)
6378
if err != nil {
6479
return fmt.Errorf("failed to insert request log for request_id=%s timestamp_ms=%d: %w", log.RequestID, log.TimestampMs, err)
@@ -75,7 +90,7 @@ func (r *requestLogStore) List(ctx context.Context, requestID string) (ret []ent
7590
defer func() { op.Complete(retErr) }()
7691

7792
rows, err := r.db.QueryContext(ctx,
78-
"SELECT request_id, timestamp_ms, status, request_version, last_error, metadata FROM request_log WHERE request_id = ? ORDER BY timestamp_ms ASC, salt ASC",
93+
"SELECT request_id, queue, change_uri, timestamp_ms, status, request_version, last_error, metadata FROM request_log WHERE request_id = ? ORDER BY timestamp_ms ASC, salt ASC",
7994
requestID,
8095
)
8196
if err != nil {
@@ -86,13 +101,18 @@ func (r *requestLogStore) List(ctx context.Context, requestID string) (ret []ent
86101
var logs []entity.RequestLog
87102
for rows.Next() {
88103
var log entity.RequestLog
104+
var changeURIsJSON []byte
89105
var metadataJSON []byte
90106

91-
err := rows.Scan(&log.RequestID, &log.TimestampMs, &log.Status, &log.RequestVersion, &log.LastError, &metadataJSON)
107+
err := rows.Scan(&log.RequestID, &log.Queue, &changeURIsJSON, &log.TimestampMs, &log.Status, &log.RequestVersion, &log.LastError, &metadataJSON)
92108
if err != nil {
93109
return nil, fmt.Errorf("failed to scan request log row for request_id=%s: %w", requestID, err)
94110
}
95111

112+
if err := json.Unmarshal(changeURIsJSON, &log.ChangeURIs); err != nil {
113+
return nil, fmt.Errorf("failed to unmarshal change URIs for request log request_id=%s: %w", requestID, err)
114+
}
115+
96116
if err := json.Unmarshal(metadataJSON, &log.Metadata); err != nil {
97117
return nil, fmt.Errorf("failed to unmarshal metadata for request log request_id=%s: %w", requestID, err)
98118
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package mysql
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/stretchr/testify/require"
22+
"github.com/uber-go/tally"
23+
"github.com/uber/submitqueue/submitqueue/entity"
24+
)
25+
26+
func TestRequestLogStoreInsert_RejectsMissingQueue(t *testing.T) {
27+
store := NewRequestLogStore(nil, tally.NoopScope)
28+
29+
err := store.Insert(context.Background(), entity.RequestLog{
30+
RequestID: "not-an-sqid",
31+
TimestampMs: 100,
32+
Status: entity.RequestStatusAccepted,
33+
})
34+
35+
require.Error(t, err)
36+
require.Contains(t, err.Error(), "requires queue")
37+
}

submitqueue/extension/storage/mysql/schema/request_log.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
CREATE TABLE IF NOT EXISTS request_log (
22
request_id VARCHAR(255) NOT NULL,
3+
queue VARCHAR(255) NOT NULL,
4+
change_uri JSON NOT NULL,
35
timestamp_ms BIGINT NOT NULL,
46
salt BIGINT NOT NULL,
57
status VARCHAR(64) NOT NULL,

0 commit comments

Comments
 (0)