This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 7137326f6 fix(tempo): send "to" with "from" when collecting worklogs
(#9200)
7137326f6 is described below
commit 7137326f663f75380f43aeb70fdb6c702518dcf5
Author: Pablo BC <[email protected]>
AuthorDate: Thu Oct 8 16:55:35 2026 +0200
fix(tempo): send "to" with "from" when collecting worklogs (#9200)
Since #9164 a full sync asks Tempo for worklogs from timeAfter with no end
date. Tempo rejects that period on every page:
{"errors":[{"message":"Non valid period, from '2026-01-28' to 'null'"}]}
(400)
so full syncs (and the first run of each team after upgrading to beta18,
whose collector state starts fresh) fail in collect_worklogs.
Whenever the query has "from" and no "to", set "to" to the date the
collection started (one value for every page). Incremental runs keep using
updatedFrom, which Tempo accepts on its own.
---
backend/plugins/tempo/tasks/worklog_collector.go | 12 ++++++++---
.../plugins/tempo/tasks/worklog_collector_test.go | 25 +++++++++++++++-------
2 files changed, 26 insertions(+), 11 deletions(-)
diff --git a/backend/plugins/tempo/tasks/worklog_collector.go
b/backend/plugins/tempo/tasks/worklog_collector.go
index 0571956ec..3e56b9a65 100644
--- a/backend/plugins/tempo/tasks/worklog_collector.go
+++ b/backend/plugins/tempo/tasks/worklog_collector.go
@@ -64,6 +64,8 @@ func CollectWorklogs(taskCtx plugin.SubTaskContext)
errors.Error {
urlTemplate = fmt.Sprintf("worklogs/team/%d",
data.Options.TeamId)
}
+ // one period end for every page, even if the collection crosses
midnight
+ startedAt := time.Now()
err = apiCollector.InitCollector(api.ApiCollectorArgs{
RawDataSubTaskArgs: rawDataSubTaskArgs,
ApiClient: data.ApiClient,
@@ -74,7 +76,7 @@ func CollectWorklogs(taskCtx plugin.SubTaskContext)
errors.Error {
// fetched until one comes back short.
Query: func(reqData *api.RequestData) (url.Values,
errors.Error) {
return buildWorklogQuery(data.Options, reqData.Pager,
- apiCollector.IsIncremental(),
apiCollector.GetSince()), nil
+ apiCollector.IsIncremental(),
apiCollector.GetSince(), startedAt), nil
},
ResponseParser: func(res *http.Response) ([]json.RawMessage,
errors.Error) {
var response struct {
@@ -98,8 +100,9 @@ func CollectWorklogs(taskCtx plugin.SubTaskContext)
errors.Error {
// buildWorklogQuery builds the query for one page of worklogs. Explicit
// fromDate/toDate options win; otherwise an incremental run asks for what
// changed since the last successful collection (updatedFrom) and a full sync
-// starts at the sync policy's timeAfter (from).
-func buildWorklogQuery(opts *TempoOptions, pager *api.Pager, incremental bool,
since *time.Time) url.Values {
+// starts at the sync policy's timeAfter (from). Tempo rejects a period with
+// "from" but no "to" ("Non valid period ... to 'null'"), so "to" defaults to
now.
+func buildWorklogQuery(opts *TempoOptions, pager *api.Pager, incremental bool,
since *time.Time, now time.Time) url.Values {
if pager == nil {
pager = &api.Pager{Page: 1, Skip: 0, Size: 1000}
}
@@ -120,5 +123,8 @@ func buildWorklogQuery(opts *TempoOptions, pager
*api.Pager, incremental bool, s
case since != nil:
query.Set("from", since.UTC().Format("2006-01-02"))
}
+ if query.Get("from") != "" && query.Get("to") == "" {
+ query.Set("to", now.UTC().Format("2006-01-02"))
+ }
return query
}
diff --git a/backend/plugins/tempo/tasks/worklog_collector_test.go
b/backend/plugins/tempo/tasks/worklog_collector_test.go
index d484d0d88..6fef72997 100644
--- a/backend/plugins/tempo/tasks/worklog_collector_test.go
+++ b/backend/plugins/tempo/tasks/worklog_collector_test.go
@@ -29,41 +29,50 @@ func TestBuildWorklogQuery(t *testing.T) {
pager := &api.Pager{Page: 3, Skip: 2000, Size: 1000}
timeAfter := time.Date(2026, 1, 28, 0, 0, 0, 0, time.UTC)
lastSuccess := time.Date(2026, 9, 22, 0, 0, 13, 0, time.UTC)
+ now := time.Date(2026, 10, 7, 15, 0, 0, 0, time.UTC)
t.Run("pages with offset and limit", func(t *testing.T) {
- q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false,
nil)
+ q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false,
nil, now)
assert.Equal(t, "2000", q.Get("offset"))
assert.Equal(t, "1000", q.Get("limit"))
})
- t.Run("full sync starts at timeAfter, without a rolling window", func(t
*testing.T) {
- q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false,
&timeAfter)
+ t.Run("full sync covers timeAfter up to today, without a rolling
window", func(t *testing.T) {
+ // Tempo rejects "from" without "to": "Non valid period, from
'2026-01-28' to 'null'" (400)
+ q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false,
&timeAfter, now)
assert.Equal(t, "2026-01-28", q.Get("from"))
- assert.Empty(t, q.Get("to"))
+ assert.Equal(t, "2026-10-07", q.Get("to"))
assert.Empty(t, q.Get("updatedFrom"))
})
t.Run("incremental sync fetches what changed since the last success",
func(t *testing.T) {
- q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, true,
&lastSuccess)
+ q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, true,
&lastSuccess, now)
assert.Equal(t, "2026-09-22T00:00:13Z", q.Get("updatedFrom"))
assert.Empty(t, q.Get("from"))
})
t.Run("global endpoint gets the same bounds", func(t *testing.T) {
- q := buildWorklogQuery(&TempoOptions{}, pager, false,
&timeAfter)
+ q := buildWorklogQuery(&TempoOptions{}, pager, false,
&timeAfter, now)
assert.Equal(t, "2026-01-28", q.Get("from"))
+ assert.Equal(t, "2026-10-07", q.Get("to"))
+ })
+
+ t.Run("a fromDate option alone also ends today", func(t *testing.T) {
+ q := buildWorklogQuery(&TempoOptions{TeamId: 4, FromDate:
"2026-03-01"}, pager, false, nil, now)
+ assert.Equal(t, "2026-03-01", q.Get("from"))
+ assert.Equal(t, "2026-10-07", q.Get("to"))
})
t.Run("explicit fromDate/toDate options win", func(t *testing.T) {
opts := &TempoOptions{TeamId: 4, FromDate: "2026-03-01",
ToDate: "2026-03-31"}
- q := buildWorklogQuery(opts, pager, true, &lastSuccess)
+ q := buildWorklogQuery(opts, pager, true, &lastSuccess, now)
assert.Equal(t, "2026-03-01", q.Get("from"))
assert.Equal(t, "2026-03-31", q.Get("to"))
assert.Empty(t, q.Get("updatedFrom"))
})
t.Run("no bounds when nothing is known", func(t *testing.T) {
- q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false,
nil)
+ q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false,
nil, now)
assert.Empty(t, q.Get("from"))
assert.Empty(t, q.Get("updatedFrom"))
})