Skip to content

Commit f5f5313

Browse files
authored
Merge pull request #2644 from target/msg-logs-paginate
admin: add pagination to message logs query
2 parents 59198a5 + 1deadac commit f5f5313

File tree

20 files changed

+1058
-532
lines changed

20 files changed

+1058
-532
lines changed

graphql2/generated.go

Lines changed: 385 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

graphql2/graphqlapp/messagelog.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package graphqlapp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/google/uuid"
9+
"github.com/target/goalert/graphql2"
10+
"github.com/target/goalert/notification"
11+
"github.com/target/goalert/notificationchannel"
12+
"github.com/target/goalert/search"
13+
"github.com/target/goalert/validation/validate"
14+
)
15+
16+
type MessageLog App
17+
18+
func (a *App) formatNC(ctx context.Context, id string) (string, error) {
19+
if id == "" {
20+
return "", nil
21+
}
22+
uid, err := uuid.Parse(id)
23+
if err != nil {
24+
return "", err
25+
}
26+
27+
n, err := a.FindOneNC(ctx, uid)
28+
if err != nil {
29+
return "", err
30+
}
31+
var typeName string
32+
switch n.Type {
33+
case notificationchannel.TypeSlack:
34+
typeName = "Slack"
35+
default:
36+
typeName = string(n.Type)
37+
}
38+
39+
return fmt.Sprintf("%s (%s)", n.Name, typeName), nil
40+
}
41+
42+
func (q *Query) formatDest(ctx context.Context, dst notification.Dest) (string, error) {
43+
if !dst.Type.IsUserCM() {
44+
return (*App)(q).formatNC(ctx, dst.ID)
45+
}
46+
47+
var str strings.Builder
48+
str.WriteString((*App)(q).FormatDestFunc(ctx, dst.Type, dst.Value))
49+
switch dst.Type {
50+
case notification.DestTypeSMS:
51+
str.WriteString(" (SMS)")
52+
case notification.DestTypeUserEmail:
53+
str.WriteString(" (Email)")
54+
case notification.DestTypeVoice:
55+
str.WriteString(" (Voice)")
56+
case notification.DestTypeUserWebhook:
57+
str.Reset()
58+
str.WriteString("Webhook")
59+
default:
60+
str.Reset()
61+
str.WriteString(dst.Type.String())
62+
}
63+
64+
return str.String(), nil
65+
}
66+
67+
func msgStatus(stat notification.Status) string {
68+
var str strings.Builder
69+
switch stat.State {
70+
case notification.StateBundled:
71+
str.WriteString("Bundled")
72+
case notification.StateUnknown:
73+
str.WriteString("Unknown")
74+
case notification.StateSending:
75+
str.WriteString("Sending")
76+
case notification.StatePending:
77+
str.WriteString("Pending")
78+
case notification.StateSent:
79+
str.WriteString("Sent")
80+
case notification.StateDelivered:
81+
str.WriteString("Delivered")
82+
case notification.StateFailedTemp:
83+
str.WriteString("Failed (temporary)")
84+
case notification.StateFailedPerm:
85+
str.WriteString("Failed (permanent)")
86+
}
87+
if stat.Details != "" {
88+
str.WriteString(": ")
89+
str.WriteString(stat.Details)
90+
}
91+
return str.String()
92+
}
93+
94+
func (q *Query) MessageLogs(ctx context.Context, opts *graphql2.MessageLogSearchOptions) (conn *graphql2.MessageLogConnection, err error) {
95+
if opts == nil {
96+
opts = &graphql2.MessageLogSearchOptions{}
97+
}
98+
var searchOpts notification.SearchOptions
99+
if opts.Search != nil {
100+
searchOpts.Search = *opts.Search
101+
}
102+
searchOpts.Omit = opts.Omit
103+
if opts.After != nil && *opts.After != "" {
104+
err = search.ParseCursor(*opts.After, &searchOpts)
105+
if err != nil {
106+
return nil, err
107+
}
108+
}
109+
if opts.First != nil {
110+
err := validate.Range("First", *opts.First, 0, 100)
111+
if err != nil {
112+
return nil, err
113+
}
114+
searchOpts.Limit = *opts.First
115+
}
116+
if opts.CreatedAfter != nil {
117+
searchOpts.CreatedAfter = *opts.CreatedAfter
118+
}
119+
if opts.CreatedBefore != nil {
120+
searchOpts.CreatedBefore = *opts.CreatedBefore
121+
}
122+
if searchOpts.Limit == 0 {
123+
searchOpts.Limit = 50
124+
}
125+
126+
searchOpts.Limit++
127+
logs, err := q.NotificationStore.Search(ctx, &searchOpts)
128+
hasNextPage := len(logs) == searchOpts.Limit
129+
searchOpts.Limit-- // prevent confusion later
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
conn = new(graphql2.MessageLogConnection)
135+
conn.PageInfo = &graphql2.PageInfo{
136+
HasNextPage: hasNextPage,
137+
}
138+
139+
if hasNextPage {
140+
last := logs[len(logs)-1]
141+
searchOpts.After.CreatedAt = last.CreatedAt
142+
searchOpts.After.ID = last.ID
143+
144+
cur, err := search.Cursor(searchOpts)
145+
if err != nil {
146+
return conn, err
147+
}
148+
conn.PageInfo.EndCursor = &cur
149+
}
150+
151+
if len(logs) > searchOpts.Limit {
152+
// If we have next page, we've fetched MORE than one page, but we only want to return one page.
153+
logs = logs[:searchOpts.Limit]
154+
}
155+
156+
for _, _log := range logs {
157+
log := _log
158+
var dest notification.Dest
159+
switch {
160+
case log.ContactMethodID != "":
161+
cm, err := (*App)(q).FindOneCM(ctx, log.ContactMethodID)
162+
if err != nil {
163+
return nil, fmt.Errorf("lookup contact method %s: %w", log.ContactMethodID, err)
164+
}
165+
dest = notification.DestFromPair(cm, nil)
166+
167+
case log.ChannelID != uuid.Nil:
168+
nc, err := (*App)(q).FindOneNC(ctx, log.ChannelID)
169+
if err != nil {
170+
return nil, fmt.Errorf("lookup notification channel %s: %w", log.ChannelID, err)
171+
}
172+
dest = notification.DestFromPair(nil, nc)
173+
}
174+
175+
dm := graphql2.DebugMessage{
176+
ID: log.ID,
177+
CreatedAt: log.CreatedAt,
178+
UpdatedAt: log.LastStatusAt,
179+
Type: strings.TrimPrefix(log.MessageType.String(), "MessageType"),
180+
Status: msgStatus(notification.Status{State: log.LastStatus, Details: log.StatusDetails}),
181+
AlertID: &log.AlertID,
182+
}
183+
if dest.ID != "" {
184+
dm.Destination, err = q.formatDest(ctx, dest)
185+
if err != nil {
186+
return nil, fmt.Errorf("format dest: %w", err)
187+
}
188+
}
189+
if log.UserID != "" {
190+
dm.UserID = &log.UserID
191+
}
192+
if log.UserName != "" {
193+
dm.UserName = &log.UserName
194+
}
195+
if log.SrcValue != "" {
196+
src, err := q.formatDest(ctx, notification.Dest{Type: dest.Type, Value: log.SrcValue})
197+
if err != nil {
198+
return nil, fmt.Errorf("format src: %w", err)
199+
}
200+
dm.Source = &src
201+
}
202+
if log.ServiceID != "" {
203+
dm.ServiceID = &log.ServiceID
204+
}
205+
if log.ServiceName != "" {
206+
dm.ServiceName = &log.ServiceName
207+
}
208+
if log.AlertID != 0 {
209+
dm.AlertID = &log.AlertID
210+
}
211+
if log.ProviderMsgID != nil {
212+
dm.ProviderID = &log.ProviderMsgID.ExternalID
213+
}
214+
215+
conn.Nodes = append(conn.Nodes, dm)
216+
}
217+
218+
return conn, nil
219+
}
220+
221+
func (q *Query) DebugMessages(ctx context.Context, input *graphql2.DebugMessagesInput) ([]graphql2.DebugMessage, error) {
222+
if input.First != nil && *input.First > 100 {
223+
*input.First = 100
224+
}
225+
conn, err := q.MessageLogs(ctx, &graphql2.MessageLogSearchOptions{
226+
CreatedBefore: input.CreatedBefore,
227+
CreatedAfter: input.CreatedAfter,
228+
First: input.First,
229+
})
230+
if err != nil {
231+
return nil, err
232+
}
233+
234+
return conn.Nodes, nil
235+
}

0 commit comments

Comments
 (0)