Repository: cloudflare/cloudflare-go
Pull request #1026 (2022-08-08 22:33:01)

Add support for Workers tail API
## Description

Adds support for the Workers tail API
https://api.cloudflare.com/#worker-tail-logs-start-tail

## Has your change been tested?

Unit tested created and pass

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [X] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [X] I have added tests to cover my changes.
- [X] All new and existing tests passed.
- [X] This change is using publicly documented (api.cloudflare.com or developers.cloudflare.com) and stable APIs.



Patch:
diff --git a/.changelog/1026.txt b/.changelog/1026.txt
new file mode 100644
index 000000000..3a43ce2de
--- /dev/null
+++ b/.changelog/1026.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+workers-tail: Add in support for Workers tail API
+```
\ No newline at end of file
diff --git a/workers_tail.go b/workers_tail.go
new file mode 100644
index 000000000..672f82191
--- /dev/null
+++ b/workers_tail.go
@@ -0,0 +1,112 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+var (
+	ErrMissingScriptName = errors.New("required script name missing")
+	ErrMissingTailID     = errors.New("required tail id missing")
+)
+
+type WorkersTail struct {
+	ID        string     `json:"id"`
+	URL       string     `json:"url"`
+	ExpiresAt *time.Time `json:"expires_at"`
+}
+
+type StartWorkersTailResponse struct {
+	Response
+	Result WorkersTail
+}
+
+type ListWorkersTailParameters struct {
+	AccountID  string
+	ScriptName string
+}
+
+type ListWorkersTailResponse struct {
+	Response
+	Result WorkersTail
+}
+
+// StartWorkersTail Starts a tail that receives logs and exception from a Worker
+//
+// API reference: https://api.cloudflare.com/#worker-tail-logs-start-tail
+func (api *API) StartWorkersTail(ctx context.Context, rc *ResourceContainer, scriptName string) (WorkersTail, error) {
+	if rc.Identifier == "" {
+		return WorkersTail{}, ErrMissingAccountID
+	}
+	if scriptName == "" {
+		return WorkersTail{}, ErrMissingScriptName
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/tails", rc.Identifier, scriptName)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil)
+	if err != nil {
+		return WorkersTail{}, err
+	}
+
+	var workerstailResponse StartWorkersTailResponse
+	if err := json.Unmarshal(res, &workerstailResponse); err != nil {
+		return WorkersTail{}, err
+	}
+
+	return workerstailResponse.Result, nil
+}
+
+// ListWorkersTail Get list of tails currently deployed on a worker
+//
+// API reference: https://api.cloudflare.com/#worker-tail-logs-list-tails
+func (api *API) ListWorkersTail(ctx context.Context, rc *ResourceContainer, params ListWorkersTailParameters) (WorkersTail, error) {
+	if rc.Identifier == "" {
+		return WorkersTail{}, ErrMissingAccountID
+	}
+
+	if params.ScriptName == "" {
+		return WorkersTail{}, ErrMissingScriptName
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/tails", rc.Identifier, params.ScriptName)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return WorkersTail{}, err
+	}
+
+	var workerstailResponse ListWorkersTailResponse
+	if err := json.Unmarshal(res, &workerstailResponse); err != nil {
+		return WorkersTail{}, err
+	}
+
+	return workerstailResponse.Result, nil
+}
+
+// DeleteWorkersTail Deletes a tail from a Worker
+//
+// API reference: https://api.cloudflare.com/#worker-tail-logs-delete-tail
+func (api *API) DeleteWorkersTail(ctx context.Context, rc *ResourceContainer, scriptName, tailID string) error {
+	if rc.Identifier == "" {
+		return ErrMissingAccountID
+	}
+
+	if scriptName == "" {
+		return ErrMissingScriptName
+	}
+
+	if tailID == "" {
+		return ErrMissingTailID
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/tails/%s", rc.Identifier, scriptName, tailID)
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}


Test patch:
diff --git a/workers_tail_test.go b/workers_tail_test.go
new file mode 100644
index 000000000..ed57b4d80
--- /dev/null
+++ b/workers_tail_test.go
@@ -0,0 +1,118 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	testScriptName = "this-is_my_script-01"
+	testTailID     = "03dc9f77817b488fb26c5861ec18f791"
+)
+
+func TestWorkersTail_StartWorkersTail(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/accounts/%s/workers/scripts/%s/tails", testAccountID, testScriptName), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+    "id": "03dc9f77817b488fb26c5861ec18f791",
+    "url": "wss://tail.developers.workers.dev/03dc9f77817b488fb26c5861ec18f791",
+    "expires_at": "2021-08-20T19:15:51Z"
+  }
+}`)
+	})
+
+	_, err := client.StartWorkersTail(context.Background(), AccountIdentifier(testAccountID), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingScriptName, err)
+	}
+
+	res, err := client.StartWorkersTail(context.Background(), AccountIdentifier(testAccountID), testScriptName)
+	expiresAt, _ := time.Parse(time.RFC3339, "2021-08-20T19:15:51Z")
+	want := WorkersTail{
+		ID:        "03dc9f77817b488fb26c5861ec18f791",
+		URL:       "wss://tail.developers.workers.dev/03dc9f77817b488fb26c5861ec18f791",
+		ExpiresAt: &expiresAt,
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, res)
+	}
+}
+
+func TestWorkersTail_ListWorkersTail(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/accounts/%s/workers/scripts/%s/tails", testAccountID, testScriptName), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+    "id": "03dc9f77817b488fb26c5861ec18f791",
+    "url": "wss://tail.developers.workers.dev/03dc9f77817b488fb26c5861ec18f791",
+    "expires_at": "2021-08-20T19:15:51Z"
+  }
+}`)
+	})
+
+	_, err := client.ListWorkersTail(context.Background(), AccountIdentifier(testAccountID), ListWorkersTailParameters{ScriptName: ""})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingScriptName, err)
+	}
+
+	res, err := client.ListWorkersTail(context.Background(), AccountIdentifier(testAccountID), ListWorkersTailParameters{ScriptName: testScriptName})
+	expiresAt, _ := time.Parse(time.RFC3339, "2021-08-20T19:15:51Z")
+	want := WorkersTail{
+		ID:        "03dc9f77817b488fb26c5861ec18f791",
+		URL:       "wss://tail.developers.workers.dev/03dc9f77817b488fb26c5861ec18f791",
+		ExpiresAt: &expiresAt,
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, res)
+	}
+}
+
+func TestWorkersTail_DeleteWorkersTail(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc(fmt.Sprintf("/accounts/%s/workers/scripts/%s/tails/%s", testAccountID, testScriptName, testTailID), func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+  "success": true,
+  "errors": [],
+  "messages": [],
+}`)
+	})
+
+	err := client.DeleteWorkersTail(context.Background(), AccountIdentifier(testAccountID), "", "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingScriptName, err)
+	}
+
+	err = client.DeleteWorkersTail(context.Background(), AccountIdentifier(testAccountID), testScriptName, "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingTailID, err)
+	}
+
+	err = client.DeleteWorkersTail(context.Background(), AccountIdentifier(testAccountID), testScriptName, testTailID)
+	assert.NoError(t, err)
+}