forked from fastly/go-fastly
/
event_logs.go
207 lines (177 loc) · 6.07 KB
/
event_logs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package fastly
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"strconv"
"time"
"github.com/google/jsonapi"
)
// Events represents an event_logs item response from the Fastly API.
type Event struct {
ID string `jsonapi:"primary,event"`
CustomerID string `jsonapi:"attr,customer_id"`
Description string `jsonapi:"attr,description"`
EventType string `jsonapi:"attr,event_type"`
IP string `jsonapi:"attr,ip"`
Metadata map[string]interface{} `jsonapi:"attr,metadata,omitempty"`
ServiceID string `jsonapi:"attr,service_id"`
UserID string `jsonapi:"attr,user_id"`
CreatedAt *time.Time `jsonapi:"attr,created_at,iso8601"`
Admin bool `jsonapi:"attr,admin"`
}
// GetAPIEventsFilter is used as input to the GetAPIEvents function.
type GetAPIEventsFilterInput struct {
// CustomerID to Limit the returned events to a specific customer.
CustomerID string
// ServiceID to Limit the returned events to a specific service.
ServiceID string
// EventType to Limit the returned events to a specific event type. See above for event codes.
EventType string
// UserID to Limit the returned events to a specific user.
UserID string
// Number is the Pagination page number.
PageNumber int
// Size is the Number of items to return on each paginated page.
MaxResults int
}
// eventLinksResponse is used to pull the "Links" pagination fields from
// a call to Fastly; these are excluded from the results of the jsonapi
// call to `UnmarshalManyPayload()`, so we have to fetch them separately.
// type EventLinksResponse struct {
// Links EventsPaginationInfo `json:"links"`
// }
// EventsPaginationInfo stores links to searches related to the current one, showing
// any information about additional results being stored on another page
type EventsPaginationInfo struct {
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
Next string `json:"next,omitempty"`
}
// GetAPIEventsResponse is the data returned to the user from a GetAPIEvents call
type GetAPIEventsResponse struct {
Events []*Event
Links EventsPaginationInfo `json:"links"`
}
// GetAPIEvents lists all the events for a particular customer
func (c *Client) GetAPIEvents(i *GetAPIEventsFilterInput) (GetAPIEventsResponse, error) {
eventsResponse := GetAPIEventsResponse{
Events: []*Event{},
Links: EventsPaginationInfo{},
}
path := fmt.Sprintf("/events")
filters := &RequestOptions{Params: i.formatEventFilters()}
resp, err := c.Get(path, filters)
if err != nil {
return eventsResponse, err
}
err = c.interpretAPIEventsPage(&eventsResponse, i.PageNumber, resp)
// NOTE: It's possible for eventsResponse to be partially completed before an error
// was encountered, so the presence of a statusResponse doesn't preclude the presence of
// an error.
// }
return eventsResponse, err
}
// GetAPIEventInput is used as input to the GetAPIEvent function.
type GetAPIEventInput struct {
// EventID is the ID of the event and is required.
EventID string
}
// GetAPIEvent gets a specific event
func (c *Client) GetAPIEvent(i *GetAPIEventInput) (*Event, error) {
if i.EventID == "" {
return nil, ErrMissingEventID
}
path := fmt.Sprintf("/events/%s", i.EventID)
resp, err := c.Get(path, nil)
if err != nil {
return nil, err
}
var event Event
if err := jsonapi.UnmarshalPayload(resp.Body, &event); err != nil {
return nil, err
}
return &event, nil
}
// interpretAPIEventsPage accepts a Fastly response for a set of WAF rule statuses
// and unmarshals the results. If there are more pages of results, it fetches the next
// page, adds that response to the array of results, and repeats until all results have
// been fetched.
func (c *Client) interpretAPIEventsPage(answer *GetAPIEventsResponse, pageNum int, received *http.Response) error {
// before we pull the status info out of the response body, fetch
// pagination info from it:
pages, body, err := getEventsPages(received.Body)
if err != nil {
return err
}
answer.Links = pages
data, err := jsonapi.UnmarshalManyPayload(body, reflect.TypeOf(new(Event)))
if err != nil {
return err
}
for i := range data {
typed, ok := data[i].(*Event)
if !ok {
return fmt.Errorf("got back response of unexpected type")
}
answer.Events = append(answer.Events, typed)
}
if pageNum == 0 {
if pages.Next != "" {
// NOTE: pages.Next URL includes filters already
resp, err := c.SimpleGet(pages.Next)
if err != nil {
return err
}
c.interpretAPIEventsPage(answer, pageNum, resp)
}
return nil
}
return nil
}
// getEventsPages parses a response to get the pagination data without destroying
// the reader we receive as "resp.Body"; this essentially copies resp.Body
// and returns it so we can use it again.
func getEventsPages(body io.Reader) (EventsPaginationInfo, io.Reader, error) {
var buf bytes.Buffer
tee := io.TeeReader(body, &buf)
bodyBytes, err := ioutil.ReadAll(tee)
if err != nil {
return EventsPaginationInfo{}, nil, err
}
var pages *GetAPIEventsResponse
json.Unmarshal(bodyBytes, &pages)
return pages.Links, bytes.NewReader(buf.Bytes()), nil
}
// formatEventFilters converts user input into query parameters for filtering
// Fastly events.
func (i *GetAPIEventsFilterInput) formatEventFilters() map[string]string {
result := map[string]string{}
pairings := map[string]interface{}{
"filter[customer_id]": i.CustomerID,
"filter[service_id]": i.ServiceID,
"filter[event_type]": i.EventType,
"filter[user_id]": i.UserID,
"page[size]": i.MaxResults,
"page[number]": i.PageNumber, // starts at 1, not 0
}
// NOTE: This setup means we will not be able to send the zero value
// of any of these filters. It doesn't appear we would need to at present.
for key, value := range pairings {
switch t := reflect.TypeOf(value).String(); t {
case "string":
if value != "" {
result[key] = value.(string)
}
case "int":
if value != 0 {
result[key] = strconv.Itoa(value.(int))
}
}
}
return result
}