Skip to content

Commit b52b838

Browse files
Philipp BöschenPhilipp Böschen
andauthored
Refactor lots of small things (#14)
* Make healthcheck respond with 200 OK instead of writing OK into the string * Add error handling everywhere * Make json library parse timestamps * GitLab does do RFC3339 compliant timestamps in most setups * ioutil is deprecated, moved to io * Clear up event type handling * Move libhoney init to main function * Move all fmt.Printf thingies to log.Printf Co-authored-by: Philipp Böschen <[email protected]>
1 parent 7643c2f commit b52b838

File tree

4 files changed

+117
-144
lines changed

4 files changed

+117
-144
lines changed

job.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"build_name": "compile",
99
"build_stage": "build",
1010
"build_status": "success",
11-
"build_created_at": "2021-08-13 11:05:27 UTC",
12-
"build_started_at": "2021-08-13 11:06:11 UTC",
13-
"build_finished_at": "2021-08-13 11:06:51 UTC",
11+
"build_created_at": "2021-08-13T11:05:27.000Z",
12+
"build_started_at": "2021-08-13T11:06:11.000Z",
13+
"build_finished_at": "2021-08-13T11:06:51.000Z",
1414
"build_duration": 39.260385,
1515
"build_queued_duration": 0.268497,
1616
"build_allow_failure": false,
@@ -45,8 +45,8 @@
4545
"author_url": "https://gitlab.com/zoidyzoidzoid",
4646
"status": "success",
4747
"duration": 82,
48-
"started_at": "2021-08-13 11:05:28 UTC",
49-
"finished_at": "2021-08-13 11:06:51 UTC"
48+
"started_at": "2021-08-13T11:05:28.000Z",
49+
"finished_at": "2021-08-13T11:06:51.000Z"
5050
},
5151
"repository": {
5252
"name": "sample-gitlab-project",

main.go

Lines changed: 98 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"crypto/md5"
55
"encoding/hex"
66
"encoding/json"
7+
"errors"
78
"fmt"
8-
"io/ioutil"
9+
"io"
910
"log"
1011
"net/http"
1112
"os"
@@ -17,51 +18,39 @@ import (
1718
"github.com/spf13/cobra"
1819
)
1920

20-
// This is the default value that should be overridden in the
21+
// Version is the default value that should be overridden in the
2122
// build/release process.
2223
var Version = "dev"
2324

24-
func home(w http.ResponseWriter, req *http.Request) {
25-
fmt.Fprintf(w, `# GitLab Honeycomb Buildevents Webhooks Sink
25+
func home(w http.ResponseWriter, _ *http.Request) {
26+
_, err := fmt.Fprintf(w, `# GitLab Honeycomb Buildevents Webhooks Sink
2627
2728
GET /healthz: healthcheck
2829
2930
POST /api/message: receive array of notifications
3031
`)
32+
if err != nil {
33+
log.Printf("home: failed to write to http response writer: %s", err)
34+
}
3135
}
3236

33-
func healthz(w http.ResponseWriter, req *http.Request) {
34-
fmt.Fprintf(w, "OK\n")
37+
func healthz(w http.ResponseWriter, _ *http.Request) {
38+
w.WriteHeader(http.StatusOK)
3539
}
3640

37-
func createEvent(cfg *libhoney.Config) *libhoney.Event {
41+
func createEvent(cfg *libhoney.Config) (*libhoney.Event, error) {
3842
libhoney.UserAgentAddition = fmt.Sprintf("buildevents/%s", Version)
3943
libhoney.UserAgentAddition += fmt.Sprintf(" (%s)", "GitLab-CI")
4044

4145
if cfg.APIKey == "" {
4246
cfg.Transmission = &transmission.WriterSender{}
4347
}
44-
libhoney.Init(*cfg)
4548

4649
ev := libhoney.NewEvent()
4750
ev.AddField("ci_provider", "GitLab-CI")
4851
ev.AddField("meta.version", Version)
4952

50-
return ev
51-
}
52-
53-
func parseTime(dt string) (*time.Time, error) {
54-
var timestamp time.Time
55-
// Try GitLab upstream datetime format
56-
timestamp, err := time.Parse("2006-01-02 15:04:05 MST", dt)
57-
if err != nil {
58-
// Try our GitLab Enterprise datetime format
59-
timestamp, err = time.Parse("2006-01-02 15:04:05 -0700", dt)
60-
if err != nil {
61-
return nil, err
62-
}
63-
}
64-
return &timestamp, nil
53+
return ev, nil
6554
}
6655

6756
func createTraceFromPipeline(cfg *libhoney.Config, p Pipeline) (*libhoney.Event, error) {
@@ -72,10 +61,14 @@ func createTraceFromPipeline(cfg *libhoney.Config, p Pipeline) (*libhoney.Event,
7261
return nil, nil
7362
}
7463
traceID := fmt.Sprint(p.ObjectAttributes.ID)
75-
ev := createEvent(cfg)
64+
ev, err := createEvent(cfg)
65+
if err != nil {
66+
return nil, err
67+
}
68+
7669
defer ev.Send()
7770
buildURL := fmt.Sprintf("%s/-/pipelines/%d", p.Project.WebURL, p.ObjectAttributes.ID)
78-
ev.Add(map[string]interface{}{
71+
err = ev.Add(map[string]interface{}{
7972
// Basic trace information
8073
"service_name": "pipeline",
8174
"trace.span_id": traceID,
@@ -98,16 +91,15 @@ func createTraceFromPipeline(cfg *libhoney.Config, p Pipeline) (*libhoney.Event,
9891
"duration_ms": p.ObjectAttributes.Duration * 1000,
9992
"queued_duration_ms": p.ObjectAttributes.QueuedDuration * 1000,
10093
})
101-
102-
timestamp, err := parseTime(p.ObjectAttributes.CreatedAt)
103-
// This error handling is a bit janky, I should tidy it up
10494
if err != nil {
105-
log.Println("Failed to parse timestamp:", err)
106-
fmt.Printf("%+v\n", ev)
107-
return ev, err
95+
return nil, fmt.Errorf("failed to add fields to event: %w", err)
10896
}
109-
ev.Timestamp = *timestamp
110-
fmt.Printf("%+v\n", ev)
97+
98+
if p.ObjectAttributes.CreatedAt.IsZero() {
99+
return nil, errors.New("Pipeline.ObjectAttributes.CreatedAt is zero")
100+
}
101+
ev.Timestamp = p.ObjectAttributes.CreatedAt
102+
log.Printf("%+v\n", ev)
111103
return ev, nil
112104
}
113105

@@ -122,9 +114,13 @@ func createTraceFromJob(cfg *libhoney.Config, j Job) (*libhoney.Event, error) {
122114
md5HashInBytes := md5.Sum([]byte(j.BuildName))
123115
md5HashInString := hex.EncodeToString(md5HashInBytes[:])
124116
spanID := md5HashInString
125-
ev := createEvent(cfg)
117+
ev, err := createEvent(cfg)
118+
if err != nil {
119+
return nil, err
120+
}
121+
126122
defer ev.Send()
127-
ev.Add(map[string]interface{}{
123+
err = ev.Add(map[string]interface{}{
128124
// Basic trace information
129125
"service_name": "job",
130126
"trace.span_id": spanID,
@@ -149,15 +145,15 @@ func createTraceFromJob(cfg *libhoney.Config, j Job) (*libhoney.Event, error) {
149145
"duration_ms": j.BuildDuration * 1000,
150146
"queued_duration_ms": j.BuildQueuedDuration * 1000,
151147
})
152-
timestamp, err := parseTime(j.BuildStartedAt)
153-
// This error handling is a bit janky, I should tidy it up
154148
if err != nil {
155-
log.Println("Failed to parse timestamp:", err)
156-
fmt.Printf("%+v\n", ev)
157-
return ev, err
149+
return nil, fmt.Errorf("failed to add fields to event: %w", err)
158150
}
159-
ev.Timestamp = *timestamp
160-
fmt.Printf("%+v\n", ev)
151+
152+
if j.BuildStartedAt.IsZero() {
153+
return nil, errors.New("BuildStartedAt time is not set")
154+
}
155+
ev.Timestamp = j.BuildStartedAt
156+
log.Printf("%+v\n", ev)
161157
return ev, nil
162158
}
163159

@@ -166,7 +162,7 @@ func handlePipeline(cfg *libhoney.Config, w http.ResponseWriter, body []byte) {
166162
var pipeline Pipeline
167163
err := json.Unmarshal(body, &pipeline)
168164
if err != nil {
169-
log.Print("Error unmarshalling request body.")
165+
log.Printf("Error unmarshalling request body: %s", err)
170166
_, printErr := fmt.Fprintf(w, "Error unmarshalling request body.")
171167
if printErr != nil {
172168
log.Print("Error printing error on error unmarshalling request body.")
@@ -176,10 +172,17 @@ func handlePipeline(cfg *libhoney.Config, w http.ResponseWriter, body []byte) {
176172
_, err = createTraceFromPipeline(cfg, pipeline)
177173
if err != nil {
178174
w.WriteHeader(http.StatusInternalServerError)
179-
fmt.Fprintf(w, "Error creating trace from pipeline object: %s", err)
175+
_, respErr := fmt.Fprintf(w, "Error creating trace from pipeline object: %s", err)
176+
if respErr != nil {
177+
log.Printf("failed to write error response: %s", respErr)
178+
}
180179
return
181180
}
182-
fmt.Fprintf(w, "Thanks!\n")
181+
182+
_, respErr := fmt.Fprint(w, "Thanks!\n")
183+
if respErr != nil {
184+
log.Printf("failed to write success response: %s", respErr)
185+
}
183186
}
184187

185188
// buildevents step $CI_PIPELINE_ID $STEP_SPAN_ID $STEP_START $CI_JOB_NAME
@@ -198,27 +201,37 @@ func handleJob(cfg *libhoney.Config, w http.ResponseWriter, body []byte) {
198201
_, err = createTraceFromJob(cfg, job)
199202
if err != nil {
200203
w.WriteHeader(http.StatusInternalServerError)
201-
fmt.Fprintf(w, "Error creating trace from job object: %s", err)
204+
_, respErr := fmt.Fprintf(w, "Error creating trace from job object: %s", err)
205+
if respErr != nil {
206+
log.Printf("failed to write error response: %s", respErr)
207+
}
202208
return
203209
}
204-
fmt.Fprintf(w, "Thanks!\n")
210+
211+
_, respErr := fmt.Fprint(w, "Thanks!\n")
212+
if respErr != nil {
213+
log.Printf("failed to write success response: %s", respErr)
214+
}
205215
}
206216

207217
func handleRequest(cfg *libhoney.Config, w http.ResponseWriter, req *http.Request) {
208218
if req.Method != http.MethodPost {
209219
http.Error(w, "Unsupported method", http.StatusMethodNotAllowed)
210220
return
211221
}
212-
eventHeaders := req.Header["X-Gitlab-Event"]
213-
if len(eventHeaders) < 1 {
222+
eventHeaders, exists := req.Header["X-Gitlab-Event"]
223+
if !exists {
214224
http.Error(w, "Missing header: X-Giitlab-Event", http.StatusBadRequest)
215225
return
216-
} else if len(eventHeaders) > 1 {
226+
}
227+
228+
if len(eventHeaders) > 1 {
217229
http.Error(w, "Invalid header: X-Gitlab-Event", http.StatusBadRequest)
218230
return
219231
}
232+
220233
eventType := eventHeaders[0]
221-
body, err := ioutil.ReadAll(req.Body)
234+
body, err := io.ReadAll(req.Body)
222235
if err != nil {
223236
log.Print("Error reading request body.")
224237
_, printErr := fmt.Fprintf(w, "Error reading request body.")
@@ -227,15 +240,16 @@ func handleRequest(cfg *libhoney.Config, w http.ResponseWriter, req *http.Reques
227240
}
228241
return
229242
}
230-
if eventType == "Pipeline Hook" {
231-
fmt.Println("Received pipeline webhook:", string(body))
243+
244+
switch eventType {
245+
case "Pipeline Hook":
246+
log.Println("Received pipeline webhook:", string(body))
232247
handlePipeline(cfg, w, body)
233-
} else if eventType == "Job Hook" {
234-
fmt.Println("Received job webhook:", string(body))
248+
case "Job Hook":
249+
log.Println("Received job webhook:", string(body))
235250
handleJob(cfg, w, body)
236-
} else {
251+
default:
237252
http.Error(w, fmt.Sprintf("Invalid event type: %s", eventType), http.StatusBadRequest)
238-
return
239253
}
240254
}
241255

@@ -252,17 +266,26 @@ about your Continuous Integration builds.`,
252266
root.PersistentFlags().StringVarP(&cfg.APIKey, "apikey", "k", "", "[env.BUILDEVENT_APIKEY] the Honeycomb authentication token")
253267
if apikey, ok := os.LookupEnv("BUILDEVENT_APIKEY"); ok {
254268
// https://github.com/spf13/viper/issues/461#issuecomment-366831834
255-
root.PersistentFlags().Lookup("apikey").Value.Set(apikey)
269+
err := root.PersistentFlags().Lookup("apikey").Value.Set(apikey)
270+
if err != nil {
271+
log.Fatalf("failed to configure `apikey`: %s", err)
272+
}
256273
}
257274

258275
root.PersistentFlags().StringVarP(&cfg.Dataset, "dataset", "d", "buildevents", "[env.BUILDEVENT_DATASET] the name of the Honeycomb dataset to which to send these events")
259276
if dataset, ok := os.LookupEnv("BUILDEVENT_DATASET"); ok {
260-
root.PersistentFlags().Lookup("dataset").Value.Set(dataset)
277+
err := root.PersistentFlags().Lookup("dataset").Value.Set(dataset)
278+
if err != nil {
279+
log.Fatalf("failed to configure `dataset`: %s", err)
280+
}
261281
}
262282

263283
root.PersistentFlags().StringVarP(&cfg.APIHost, "apihost", "a", "https://api.honeycomb.io", "[env.BUILDEVENT_APIHOST] the hostname for the Honeycomb API server to which to send this event")
264284
if apihost, ok := os.LookupEnv("BUILDEVENT_APIHOST"); ok {
265-
root.PersistentFlags().Lookup("apihost").Value.Set(apihost)
285+
err := root.PersistentFlags().Lookup("apihost").Value.Set(apihost)
286+
if err != nil {
287+
log.Fatalf("failed to configure `apihost`: %s", err)
288+
}
266289
}
267290

268291
return root
@@ -279,6 +302,12 @@ func main() {
279302
libhoney.Close()
280303
os.Exit(1)
281304
}
305+
306+
err := libhoney.Init(config)
307+
if err != nil {
308+
log.Fatalf("failed to initialise libhoney: %s", err)
309+
}
310+
282311
log.SetOutput(os.Stdout)
283312
mux := http.NewServeMux()
284313
mux.HandleFunc("/healthz", healthz)
@@ -297,7 +326,7 @@ func main() {
297326
ReadTimeout: 5 * time.Second,
298327
WriteTimeout: 10 * time.Second,
299328
}
300-
fmt.Printf("Starting server on http://%s\n", srv.Addr)
329+
log.Printf("Starting server on http://%s\n", srv.Addr)
301330
log.Fatal(srv.ListenAndServe())
302331
}
303332

@@ -332,9 +361,9 @@ type Build struct {
332361
Stage string `json:"stage"`
333362
Name string `json:"name"`
334363
Status string `json:"status"`
335-
CreatedAt string `json:"created_at"`
336-
StartedAt *string `json:"started_at"`
337-
FinishedAt *string `json:"finished_at"`
364+
CreatedAt time.Time `json:"created_at"`
365+
StartedAt time.Time `json:"started_at"`
366+
FinishedAt time.Time `json:"finished_at"`
338367
When string `json:"when"`
339368
Manual bool `json:"manual"`
340369
AllowFailure bool `json:"allow_failure"`
@@ -405,8 +434,8 @@ type PipelineObjectAttributes struct {
405434
Source string `json:"source"`
406435
Status string `json:"status"`
407436
Stages []string `json:"stages"`
408-
CreatedAt string `json:"created_at"`
409-
FinishedAt string `json:"finished_at"`
437+
CreatedAt time.Time `json:"created_at"`
438+
FinishedAt time.Time `json:"finished_at"`
410439
Duration int64 `json:"duration"`
411440
QueuedDuration int64 `json:"queued_duration"`
412441
Variables []Variable `json:"variables"`
@@ -457,9 +486,9 @@ type Job struct {
457486
BuildName string `json:"build_name"`
458487
BuildStage string `json:"build_stage"`
459488
BuildStatus string `json:"build_status"`
460-
BuildCreatedAt string `json:"build_created_at"`
461-
BuildStartedAt string `json:"build_started_at"`
462-
BuildFinishedAt string `json:"build_finished_at"`
489+
BuildCreatedAt time.Time `json:"build_created_at"`
490+
BuildStartedAt time.Time `json:"build_started_at"`
491+
BuildFinishedAt time.Time `json:"build_finished_at"`
463492
BuildDuration float64 `json:"build_duration"`
464493
BuildQueuedDuration float64 `json:"build_queued_duration"`
465494
BuildAllowFailure bool `json:"build_allow_failure"`

0 commit comments

Comments
 (0)