Repository: honeycombio/terraform-provider-honeycombio (language: python)

Issue:
query_result datasource should retry-and-backoff on 429 response
As the Query Data API has rate limits we should handle these retries transparently in the client up to a certain timeout.


Discussion / hints:

Fix patch:
diff --git a/client/board.go b/client/board.go
index dd2a54b0..cc2e45ed 100644
--- a/client/board.go
+++ b/client/board.go
@@ -13,8 +13,7 @@ type Boards interface {
 	// List all boards.
 	List(ctx context.Context) ([]Board, error)
 
-	// Get a board by its ID. Returns ErrNotFound if there is no board with the
-	// given ID.
+	// Get a board by its ID.
 	Get(ctx context.Context, id string) (*Board, error)
 
 	// Create a new board. When creating a new board ID may not be set.
diff --git a/client/burn_alert.go b/client/burn_alert.go
index 357d58f1..c61283a2 100644
--- a/client/burn_alert.go
+++ b/client/burn_alert.go
@@ -11,12 +11,10 @@ import (
 //
 // API docs: https://docs.honeycomb.io/api/burn-alerts/
 type BurnAlerts interface {
-	// List all BurnAlerts associated with a SLO. Returns ErrNotFound if there is no SLO with
-	// the given ID in this dataset.
+	// List all BurnAlerts associated with a SLO.
 	ListForSLO(ctx context.Context, dataset string, sloId string) ([]BurnAlert, error)
 
-	// Get a BurnAlert by its ID. Returns ErrNotFound if there is no BurnAlert with
-	// the given ID in this dataset.
+	// Get a BurnAlert by its ID.
 	Get(ctx context.Context, dataset string, id string) (*BurnAlert, error)
 
 	// Create a new BurnAlert on a SLO. When creating a BurnAlert ID may not
diff --git a/client/client.go b/client/client.go
index f6acbb57..918e35fe 100644
--- a/client/client.go
+++ b/client/client.go
@@ -10,11 +10,23 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log"
 	"net/http"
 	"net/url"
+	"os"
 	"strings"
+	"time"
 
-	"github.com/honeycombio/terraform-provider-honeycombio/client/internal/httputil"
+	cleanhttp "github.com/hashicorp/go-cleanhttp"
+	retryablehttp "github.com/hashicorp/go-retryablehttp"
+)
+
+const (
+	DefaultAPIHost   = "https://api.honeycomb.io"
+	DefaultAPIKeyEnv = "HONEYCOMB_API_KEY"
+	// Deprecated: use DefaultAPIKeyEnv instead. To be removed in v1.0
+	LegacyAPIKeyEnv  = "HONEYCOMBIO_API_KEY"
+	defaultUserAgent = "go-honeycombio"
 )
 
 // Config holds all configuration options for the client.
@@ -23,43 +35,20 @@ type Config struct {
 	APIKey string
 	// URL of the Honeycomb API, defaults to "https://api.honeycomb.io".
 	APIUrl string
-	// User agent to send with all requests, defaults to "go-honeycombio".
-	UserAgent string
 	// With debug enabled the client will log all requests and responses.
 	Debug bool
-}
-
-func defaultConfig() *Config {
-	return &Config{
-		APIKey:    "",
-		APIUrl:    "https://api.honeycomb.io",
-		UserAgent: "go-honeycombio",
-		Debug:     false,
-	}
-}
-
-// Merge the given config by copying all non-blank values.
-func (c *Config) merge(other *Config) {
-	if other.APIKey != "" {
-		c.APIKey = other.APIKey
-	}
-	if other.APIUrl != "" {
-		c.APIUrl = other.APIUrl
-	}
-	if other.UserAgent != "" {
-		c.UserAgent = other.UserAgent
-	}
-	if c.Debug || other.Debug {
-		c.Debug = true
-	}
+	// Optionally override the HTTP client with a custom client.
+	HTTPClient *http.Client
+	// Optionally set the user agent to send with all requests, defaults to "go-honeycombio".
+	UserAgent string
 }
 
 // Client to interact with Honeycomb.
 type Client struct {
 	apiKey     string
 	apiURL     *url.URL
-	userAgent  string
-	httpClient *http.Client
+	headers    http.Header
+	httpClient *retryablehttp.Client
 
 	Auth               Auth
 	Boards             Boards
@@ -78,10 +67,39 @@ type Client struct {
 	Recipients         Recipients
 }
 
+func DefaultConfig() *Config {
+	c := &Config{
+		APIKey:     os.Getenv(DefaultAPIKeyEnv),
+		APIUrl:     DefaultAPIHost,
+		Debug:      false,
+		HTTPClient: cleanhttp.DefaultPooledClient(),
+		UserAgent:  defaultUserAgent,
+	}
+
+	// if API Key is still unset, try using the legacy environment variable
+	if c.APIKey == "" {
+		c.APIKey = os.Getenv(LegacyAPIKeyEnv)
+	}
+
+	return c
+}
+
 // NewClient creates a new Honeycomb API client.
 func NewClient(config *Config) (*Client, error) {
-	cfg := defaultConfig()
-	cfg.merge(config)
+	cfg := DefaultConfig()
+
+	if config.APIKey != "" {
+		cfg.APIKey = config.APIKey
+	}
+	if config.APIUrl != "" {
+		cfg.APIUrl = config.APIUrl
+	}
+	if config.UserAgent != "" {
+		cfg.UserAgent = config.UserAgent
+	}
+	if config.HTTPClient != nil {
+		cfg.HTTPClient = config.HTTPClient
+	}
 
 	if cfg.APIKey == "" {
 		return nil, errors.New("APIKey must be configured")
@@ -91,17 +109,34 @@ func NewClient(config *Config) (*Client, error) {
 		return nil, fmt.Errorf("could not parse APIUrl: %w", err)
 	}
 
-	httpClient := &http.Client{}
-	if cfg.Debug {
-		httpClient = httputil.WrapWithLogging(httpClient)
-	}
-
 	client := &Client{
-		apiKey:     cfg.APIKey,
-		apiURL:     apiURL,
-		userAgent:  cfg.UserAgent,
-		httpClient: httpClient,
+		apiKey:  cfg.APIKey,
+		apiURL:  apiURL,
+		headers: make(http.Header),
+	}
+
+	client.httpClient = &retryablehttp.Client{
+		Backoff:      retryablehttp.DefaultBackoff,
+		CheckRetry:   client.retryHTTPCheck,
+		ErrorHandler: retryablehttp.PassthroughErrorHandler,
+		HTTPClient:   cfg.HTTPClient,
+		RetryWaitMin: 200 * time.Millisecond,
+		RetryWaitMax: 10 * time.Second,
+		RetryMax:     15,
+	}
+
+	if config.Debug {
+		// if enabled we log all requests and responses to sterr
+		client.httpClient.Logger = log.New(os.Stderr, "", log.LstdFlags)
+		client.httpClient.ResponseLogHook = func(l retryablehttp.Logger, resp *http.Response) {
+			l.Printf("[DEBUG] Response: %s %s", resp.Request.Method, resp.Request.URL.String())
+		}
 	}
+
+	client.headers.Add("Content-Type", "application/json")
+	client.headers.Add("User-Agent", cfg.UserAgent)
+	client.headers.Add("X-Honeycomb-Team", cfg.APIKey)
+
 	client.Auth = &auth{client: client}
 	client.Boards = &boards{client: client}
 	client.Columns = &columns{client: client}
@@ -132,15 +167,13 @@ func (c *Client) IsClassic(ctx context.Context) bool {
 	return metadata.Environment.Slug == ""
 }
 
-// ErrNotFound is returned when the requested item could not be found.
-var ErrNotFound = errors.New("404 Not Found")
-
-// performRequest against the Honeycomb API with the necessary headers and, if
-// requestBody is not nil, a JSON body. The response is parsed in responseBody,
-// if responseBody is not nil.
-// Returns an error if the request failed, if the response contained a non-2xx
-// status code or if parsing the response in responseBody failed. ErrNotFound
-// is returned on a 404 response.
+// performRequest makes a request to the Honeycomb API and, if
+// requestBody is not nil, a JSON body.
+//
+// The response is parsed in responseBody, if responseBody is not nil.
+//
+// Attemps to return a DetailedError if the response status code is not 2xx,
+// but can return a generic error.
 func (c *Client) performRequest(ctx context.Context, method, path string, requestBody, responseBody interface{}) error {
 	var body io.Reader
 
@@ -158,14 +191,11 @@ func (c *Client) performRequest(ctx context.Context, method, path string, reques
 		return err
 	}
 
-	req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), body)
+	req, err := retryablehttp.NewRequestWithContext(ctx, method, requestURL.String(), body)
 	if err != nil {
 		return err
 	}
-
-	req.Header.Add("X-Honeycomb-Team", c.apiKey)
-	req.Header.Add("Content-Type", "application/json")
-	req.Header.Add("User-Agent", c.userAgent)
+	req.Header = c.headers
 
 	resp, err := c.httpClient.Do(req)
 	if err != nil {
@@ -173,65 +203,28 @@ func (c *Client) performRequest(ctx context.Context, method, path string, reques
 	}
 	defer resp.Body.Close()
 
-	if !httputil.Is2xx(resp.StatusCode) {
-		if resp.StatusCode == 404 {
-			return ErrNotFound
-		}
+	if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
 		return errorFromResponse(resp)
 	}
-
 	if responseBody != nil {
 		err = json.NewDecoder(resp.Body).Decode(responseBody)
 	}
-	return err
-}
-
-func errorFromResponse(resp *http.Response) error {
-	errorMsg := attemptToExtractHoneycombioError(resp.Body)
-	if errorMsg == "" {
-		return fmt.Errorf("%s", resp.Status)
-	}
-	return fmt.Errorf("%s: %s", resp.Status, errorMsg)
-}
-
-type honeycombioError struct {
-	Status  int    `json:"status"`
-	Err     string `json:"error"`
-	Details []struct {
-		Code        string `json:"code"`
-		Description string `json:"description"`
-	} `json:"type_detail,omitempty"`
-}
-
-func (e *honeycombioError) Error() string {
-	if len(e.Details) > 0 {
-		var response string
-		for i, d := range e.Details {
-			response += d.Code + " - " + d.Description
-			if i > len(e.Details)-1 {
-				response += ", "
-			}
-		}
-		return response
-	}
 
-	return e.Err
+	return err
 }
 
-func attemptToExtractHoneycombioError(bodyReader io.Reader) string {
-	body, err := io.ReadAll(bodyReader)
-	if err != nil {
-		return ""
+// retryHTTPCheck provides a callback for Client.CheckRetry which
+// will retry both rate limit (429) and server gateway (502, 504) errors.
+func (c *Client) retryHTTPCheck(ctx context.Context, resp *http.Response, err error) (bool, error) {
+	if ctx.Err() != nil {
+		return false, ctx.Err()
 	}
 
-	var honeycombioErr honeycombioError
-
-	err = json.Unmarshal(body, &honeycombioErr)
-	if err != nil {
-		return string(body)
+	if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusBadGateway || resp.StatusCode == http.StatusGatewayTimeout {
+		return true, nil
 	}
 
-	return honeycombioErr.Error()
+	return false, nil
 }
 
 // urlEncodeDataset sanitizes the dataset name for when it is used as part of
diff --git a/client/column.go b/client/column.go
index c9ebdf2a..52537e3c 100644
--- a/client/column.go
+++ b/client/column.go
@@ -15,12 +15,10 @@ type Columns interface {
 	// List all columns in this dataset.
 	List(ctx context.Context, dataset string) ([]Column, error)
 
-	// Get a column by its ID. Returns ErrNotFound if there is no column with
-	// the given ID in this dataset.
+	// Get a column by its ID.
 	Get(ctx context.Context, dataset string, id string) (*Column, error)
 
-	// GetByKeyName searches a column by its key name. Returns ErrNotFound if
-	// there is no column with the given key name in this dataset.
+	// GetByKeyName searches a column by its key name.
 	GetByKeyName(ctx context.Context, dataset string, keyName string) (*Column, error)
 
 	// Create a new column in this dataset. When creating a new column ID may
diff --git a/client/dataset.go b/client/dataset.go
index a47d1421..c928885d 100644
--- a/client/dataset.go
+++ b/client/dataset.go
@@ -14,8 +14,7 @@ type Datasets interface {
 	// List all datasets.
 	List(ctx context.Context) ([]Dataset, error)
 
-	// Get a dataset by its slug. Returns ErrNotFound if there is no dataset
-	// with the given slug.
+	// Get a dataset by its slug.
 	Get(ctx context.Context, slug string) (*Dataset, error)
 
 	// Create a new dataset. Only name should be set when creating a dataset,
diff --git a/client/derived_column.go b/client/derived_column.go
index 765fc437..b6539cda 100644
--- a/client/derived_column.go
+++ b/client/derived_column.go
@@ -14,12 +14,10 @@ type DerivedColumns interface {
 	// List all derived columns in this dataset.
 	List(ctx context.Context, dataset string) ([]DerivedColumn, error)
 
-	// Get a derived column by its ID. Returns ErrNotFound if there is no
-	// derived column with the given ID in this dataset.
+	// Get a derived column by its ID.
 	Get(ctx context.Context, dataset string, id string) (*DerivedColumn, error)
 
-	// GetByAlias searches a derived column by its alias. Returns ErrNotFound if
-	// there is no derived column with the given alias in this dataset.
+	// GetByAlias searches a derived column by its alias.
 	GetByAlias(ctx context.Context, dataset string, alias string) (*DerivedColumn, error)
 
 	// Create a new derived column in this dataset. When creating a new derived
diff --git a/client/errors.go b/client/errors.go
new file mode 100644
index 00000000..3c953930
--- /dev/null
+++ b/client/errors.go
@@ -0,0 +1,74 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+)
+
+// DetailedError is an RFC7807 'Problem Detail' formatted error message.
+type DetailedError struct {
+	// The HTTP status code of the error.
+	Status int `json:"status,omitempty"`
+	// The error message
+	Message string `json:"error,omitempty"`
+	// Type is a URI used to uniquely identify the type of error.
+	Type string `json:"type,omitempty"`
+	// Title is a human-readable summary that explains the type of the problem.
+	Title string `json:"title,omitempty"`
+	// Details is an array of structured objects that give details about the error.
+	Details []struct {
+		Code        string `json:"code"`
+		Description string `json:"description"`
+		Field       string `json:"field"`
+	} `json:"type_detail,omitempty"`
+}
+
+// Error returns a pretty-printed representation of the error
+func (e DetailedError) Error() string {
+	if len(e.Details) > 0 {
+		var response string
+		for i, d := range e.Details {
+			response += d.Code + " - " + d.Description
+			if i > len(e.Details)-1 {
+				response += ", "
+			}
+		}
+		return response
+	}
+
+	return e.Message
+}
+
+// IsNotFound returns true if the error is an HTTP 404
+func (e *DetailedError) IsNotFound() bool {
+	if e == nil {
+		return false
+	}
+	return e.Status == http.StatusNotFound
+}
+
+func errorFromResponse(resp *http.Response) error {
+	e, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("unable to read response body: %w", err)
+	}
+
+	var detailedErr DetailedError
+	err = json.Unmarshal(e, &detailedErr)
+	if err != nil {
+		// we failed to parse the body as a DetailedError, so build one from what we know
+		return &DetailedError{
+			Status:  resp.StatusCode,
+			Message: resp.Status,
+		}
+	}
+
+	// quick sanity check to make sure we got a StatusCode
+	if detailedErr.Status == 0 {
+		detailedErr.Status = resp.StatusCode
+	}
+
+	return detailedErr
+}
diff --git a/client/internal/httputil/httputil.go b/client/internal/httputil/httputil.go
deleted file mode 100644
index c64f8946..00000000
--- a/client/internal/httputil/httputil.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package httputil
-
-import (
-	"log"
-	"net/http"
-	"net/http/httputil"
-)
-
-// Is2xx checks whether the HTTP status code is successful.
-func Is2xx(status int) bool {
-	return status >= 200 && status < 300
-}
-
-// roundTripFunc implements http.RoundTripper and can be used as utility
-// function to simplify creating a new http.RoundTripper.
-type roundTripFunc func(*http.Request) (*http.Response, error)
-
-func (rf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
-	return rf(req)
-}
-
-// Compile-time proof of interface implementation by type roundTripFunc.
-var _ http.RoundTripper = (*roundTripFunc)(nil)
-
-// WrapWithLogging wraps the HTTP client to log the entire request and
-// response.
-func WrapWithLogging(c *http.Client) *http.Client {
-	return &http.Client{
-		Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
-			b, _ := httputil.DumpRequestOut(req, true)
-			log.Printf("Sending request\n\n%s\n", b)
-
-			res, err := c.Do(req)
-			if err != nil {
-				return res, err
-			}
-
-			b, _ = httputil.DumpResponse(res, true)
-			log.Printf("Received response\n\n%s\n", b)
-
-			return res, nil
-		}),
-	}
-}
diff --git a/client/marker.go b/client/marker.go
index 4d7c70ef..88fcf8cc 100644
--- a/client/marker.go
+++ b/client/marker.go
@@ -3,6 +3,7 @@ package client
 import (
 	"context"
 	"fmt"
+	"net/http"
 	"time"
 )
 
@@ -14,8 +15,7 @@ type Markers interface {
 	// List all markers present in this dataset.
 	List(ctx context.Context, dataset string) ([]Marker, error)
 
-	// Get a marker by its ID. Returns ErrNotFound if there is no marker with
-	// the given ID in this dataset.
+	// Get a marker by its ID.
 	//
 	// This method calls List internally since there is no API available to
 	// directly get a single marker.
@@ -88,7 +88,10 @@ func (s *markers) Get(ctx context.Context, dataset string, id string) (*Marker,
 			return &m, nil
 		}
 	}
-	return nil, ErrNotFound
+	return nil, &DetailedError{
+		Status:  http.StatusNotFound,
+		Message: "Marker Setting Not Found.",
+	}
 }
 
 func (s *markers) Create(ctx context.Context, dataset string, data *Marker) (*Marker, error) {
diff --git a/client/marker_settings.go b/client/marker_settings.go
index f9fb5197..b148a09e 100644
--- a/client/marker_settings.go
+++ b/client/marker_settings.go
@@ -3,6 +3,7 @@ package client
 import (
 	"context"
 	"fmt"
+	"net/http"
 	"time"
 )
 
@@ -14,8 +15,7 @@ type MarkerSettings interface {
 	// List all marker settings present in this dataset.
 	List(ctx context.Context, dataset string) ([]MarkerSetting, error)
 
-	// Get a marker setting by its ID. Returns ErrNotFound if there is no marker setting with
-	// the given ID in this dataset.
+	// Get a marker setting by its ID.
 	//
 	// This method calls List internally since there is no API available to
 	// directly get a single marker setting.
@@ -75,7 +75,10 @@ func (s *markerSettings) Get(ctx context.Context, dataset string, id string) (*M
 			return &m, nil
 		}
 	}
-	return nil, ErrNotFound
+	return nil, &DetailedError{
+		Status:  http.StatusNotFound,
+		Message: "Marker Setting Not Found.",
+	}
 }
 
 func (s *markerSettings) Create(ctx context.Context, dataset string, data *MarkerSetting) (*MarkerSetting, error) {
diff --git a/client/query_annotation.go b/client/query_annotation.go
index b2a7d38e..11bffc38 100644
--- a/client/query_annotation.go
+++ b/client/query_annotation.go
@@ -14,8 +14,7 @@ type QueryAnnotations interface {
 	// List all query annotations.
 	List(ctx context.Context, dataset string) ([]QueryAnnotation, error)
 
-	// Get a query annotation by its ID. Returns ErrNotFound if there is no
-	// query annotation with the given ID.
+	// Get a query annotation by its ID.
 	Get(ctx context.Context, dataset string, id string) (*QueryAnnotation, error)
 
 	// Create a new query annotation. When creating a new query annotation ID
diff --git a/client/query_result.go b/client/query_result.go
index 62346944..ff466178 100644
--- a/client/query_result.go
+++ b/client/query_result.go
@@ -11,8 +11,7 @@ import (
 //
 // API docs: https://docs.honeycomb.io/api/query-results/
 type QueryResults interface {
-	// Get the query results by ID. Returns ErrNotFound if there is no
-	// query result with the given ID.
+	// Get the query results by ID.
 	Get(ctx context.Context, dataset string, q *QueryResult) error
 
 	// Create a new query result with a given query specification.
diff --git a/client/recipient.go b/client/recipient.go
index 5a19d379..c3aa622c 100644
--- a/client/recipient.go
+++ b/client/recipient.go
@@ -13,8 +13,7 @@ type Recipients interface {
 	// List all Recipients
 	List(ctx context.Context) ([]Recipient, error)
 
-	// Get a Recipient by its ID. Returns ErrNotFound if there is no Recipient with
-	// the given ID
+	// Get a Recipient by its ID.
 	Get(ctx context.Context, id string) (*Recipient, error)
 
 	// Create a new Recipient. When creating a new Recipient ID must not be set.
diff --git a/client/slo.go b/client/slo.go
index 1571dac6..f23b16a1 100644
--- a/client/slo.go
+++ b/client/slo.go
@@ -13,8 +13,7 @@ type SLOs interface {
 	// List all SLOs present in this dataset.
 	List(ctx context.Context, dataset string) ([]SLO, error)
 
-	// Get a SLO by its ID. Returns ErrNotFound if there is no SLO with
-	// the given ID in this dataset.
+	// Get a SLO by its ID.
 	Get(ctx context.Context, dataset string, id string) (*SLO, error)
 
 	// Create a new SLO in this dataset. When creating a SLO ID may not
diff --git a/client/trigger.go b/client/trigger.go
index 98dd5da3..f61dea92 100644
--- a/client/trigger.go
+++ b/client/trigger.go
@@ -15,8 +15,7 @@ type Triggers interface {
 	// List all triggers present in this dataset.
 	List(ctx context.Context, dataset string) ([]Trigger, error)
 
-	// Get a trigger by its ID. Returns ErrNotFound if there is no trigger with
-	// the given ID in this dataset.
+	// Get a trigger by its ID.
 	Get(ctx context.Context, dataset string, id string) (*Trigger, error)
 
 	// Create a new trigger in this dataset. When creating a new trigger ID
diff --git a/go.mod b/go.mod
index 17461a2f..4e15cd80 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.20
 
 require (
 	github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
+	github.com/hashicorp/go-retryablehttp v0.7.4
 	github.com/hashicorp/terraform-plugin-framework v1.4.1
 	github.com/hashicorp/terraform-plugin-go v0.19.0
 	github.com/hashicorp/terraform-plugin-mux v0.12.0
@@ -29,7 +30,7 @@ require (
 	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-checkpoint v0.5.0 // indirect
-	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2
 	github.com/hashicorp/go-hclog v1.5.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-plugin v1.5.1 // indirect
diff --git a/go.sum b/go.sum
index b5116f55..111c0001 100644
--- a/go.sum
+++ b/go.sum
@@ -41,12 +41,15 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=
 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
 github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k=
 github.com/hashicorp/go-plugin v1.5.1/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4=
+github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
+github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
 github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
@@ -117,6 +120,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
diff --git a/honeycombio/data_source_column.go b/honeycombio/data_source_column.go
index b516d773..4c15301d 100644
--- a/honeycombio/data_source_column.go
+++ b/honeycombio/data_source_column.go
@@ -2,7 +2,6 @@ package honeycombio
 
 import (
 	"context"
-	"fmt"
 	"time"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -86,7 +85,7 @@ func dataSourceHoneycombioColumnRead(ctx context.Context, d *schema.ResourceData
 
 	column, err := client.Columns.GetByKeyName(ctx, dataset, matchName)
 	if err != nil {
-		return diag.FromErr(fmt.Errorf("column not found, API returned: %s", err.Error()))
+		return diagFromErr(err)
 	}
 
 	d.SetId(column.ID)
diff --git a/honeycombio/data_source_columns.go b/honeycombio/data_source_columns.go
index df354a5e..b930b56e 100644
--- a/honeycombio/data_source_columns.go
+++ b/honeycombio/data_source_columns.go
@@ -45,7 +45,7 @@ func dataSourceHoneycombioColumnsRead(ctx context.Context, d *schema.ResourceDat
 
 	columns, err := client.Columns.List(ctx, dataset)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	var startsWith string
diff --git a/honeycombio/data_source_datasets.go b/honeycombio/data_source_datasets.go
index 6b88f196..de3cc6c1 100644
--- a/honeycombio/data_source_datasets.go
+++ b/honeycombio/data_source_datasets.go
@@ -43,7 +43,7 @@ func dataSourceHoneycombioDatasetsRead(ctx context.Context, d *schema.ResourceDa
 
 	datasets, err := client.Datasets.List(ctx)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	var startsWith string
diff --git a/honeycombio/data_source_query_result.go b/honeycombio/data_source_query_result.go
index 1f301a08..b5ea2693 100644
--- a/honeycombio/data_source_query_result.go
+++ b/honeycombio/data_source_query_result.go
@@ -61,16 +61,16 @@ func dataSourceHoneycombioQueryResultRead(ctx context.Context, d *schema.Resourc
 	}
 	query, err := client.Queries.Create(ctx, dataset, &querySpec)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	queryResult, err := client.QueryResults.Create(ctx, dataset, &honeycombio.QueryResultRequest{ID: *query.ID})
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	err = client.QueryResults.Get(ctx, dataset, queryResult)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	results := make([]map[string]string, len(queryResult.Data.Results))
diff --git a/honeycombio/data_source_recipient.go b/honeycombio/data_source_recipient.go
index 55a24587..ea4c97dc 100644
--- a/honeycombio/data_source_recipient.go
+++ b/honeycombio/data_source_recipient.go
@@ -136,7 +136,7 @@ func dataSourceHoneycombioRecipientRead(ctx context.Context, d *schema.ResourceD
 
 	rcpts, err := client.Recipients.List(ctx)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	matchType := honeycombio.RecipientType(d.Get("type").(string))
 
diff --git a/honeycombio/data_source_recipients.go b/honeycombio/data_source_recipients.go
index b9225f5d..f69280eb 100644
--- a/honeycombio/data_source_recipients.go
+++ b/honeycombio/data_source_recipients.go
@@ -75,7 +75,7 @@ func dataSourceHoneycombioRecipientsRead(ctx context.Context, d *schema.Resource
 
 	rcpts, err := client.Recipients.List(ctx)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	var matchType honeycombio.RecipientType
diff --git a/honeycombio/data_source_trigger_recipient.go b/honeycombio/data_source_trigger_recipient.go
index 90b8436d..52b2b7f1 100644
--- a/honeycombio/data_source_trigger_recipient.go
+++ b/honeycombio/data_source_trigger_recipient.go
@@ -38,7 +38,7 @@ func dataSourceHoneycombioSlackRecipientRead(ctx context.Context, d *schema.Reso
 
 	triggers, err := client.Triggers.List(ctx, dataset)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	searchType := honeycombio.RecipientType(d.Get("type").(string))
diff --git a/honeycombio/provider.go b/honeycombio/provider.go
index 60ddbcd9..afca32f6 100644
--- a/honeycombio/provider.go
+++ b/honeycombio/provider.go
@@ -65,10 +65,10 @@ func Provider(version string) *schema.Provider {
 	}
 
 	provider.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
-		apiKey := os.Getenv("HONEYCOMB_API_KEY")
+		apiKey := os.Getenv(honeycombio.DefaultAPIKeyEnv)
 		if apiKey == "" {
 			// fall through to legacy env var
-			apiKey = os.Getenv("HONEYCOMBIO_APIKEY")
+			apiKey = os.Getenv(honeycombio.LegacyAPIKeyEnv)
 		}
 		if v, ok := d.GetOk("api_key"); ok {
 			apiKey = v.(string)
diff --git a/honeycombio/resource_board.go b/honeycombio/resource_board.go
index 4ca97b20..4b938598 100644
--- a/honeycombio/resource_board.go
+++ b/honeycombio/resource_board.go
@@ -157,7 +157,7 @@ func resourceBoardCreate(ctx context.Context, d *schema.ResourceData, meta inter
 
 	b, err = client.Boards.Create(ctx, b)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(b.ID)
@@ -167,10 +167,15 @@ func resourceBoardCreate(ctx context.Context, d *schema.ResourceData, meta inter
 func resourceBoardRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
 	client := meta.(*honeycombio.Client)
 
+	var detailedErr honeycombio.DetailedError
 	b, err := client.Boards.Get(ctx, d.Id())
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
@@ -210,7 +215,7 @@ func resourceBoardUpdate(ctx context.Context, d *schema.ResourceData, meta inter
 
 	b, err = client.Boards.Update(ctx, b)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(b.ID)
@@ -222,7 +227,7 @@ func resourceBoardDelete(ctx context.Context, d *schema.ResourceData, meta inter
 
 	err := client.Boards.Delete(ctx, d.Id())
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	return nil
 }
@@ -304,7 +309,7 @@ func expandBoardQueryGraphSettings(gs interface{}) (honeycombio.BoardGraphSettin
 }
 
 func flattenBoardQueryGraphSettings(gs honeycombio.BoardGraphSettings) []map[string]interface{} {
-	result := make([]map[string]interface{}, 0)
+	result := make([]map[string]interface{}, 0, 1)
 
 	result = append(result, map[string]interface{}{
 		"hide_markers":        gs.HideMarkers,
diff --git a/honeycombio/resource_column.go b/honeycombio/resource_column.go
index 12e2efa6..dad92540 100644
--- a/honeycombio/resource_column.go
+++ b/honeycombio/resource_column.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"strings"
 	"time"
@@ -106,7 +107,7 @@ func resourceColumnCreate(ctx context.Context, d *schema.ResourceData, meta inte
 
 	column, err := client.Columns.Create(ctx, dataset, expandColumn(d))
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(column.ID)
@@ -125,10 +126,15 @@ func resourceColumnRead(ctx context.Context, d *schema.ResourceData, meta interf
 	}
 
 	// we read by name here to faciliate importing by name instead of ID
+	var detailedErr honeycombio.DetailedError
 	column, err := client.Columns.GetByKeyName(ctx, dataset, columnName)
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
@@ -152,7 +158,7 @@ func resourceColumnUpdate(ctx context.Context, d *schema.ResourceData, meta inte
 
 	column, err := client.Columns.Update(ctx, dataset, expandColumn(d))
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(column.ID)
@@ -166,7 +172,7 @@ func resourceColumnDelete(ctx context.Context, d *schema.ResourceData, meta inte
 
 	err := client.Columns.Delete(ctx, dataset, d.Id())
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	return nil
 }
diff --git a/honeycombio/resource_dataset.go b/honeycombio/resource_dataset.go
index da881197..6d38dd23 100644
--- a/honeycombio/resource_dataset.go
+++ b/honeycombio/resource_dataset.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 	"time"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -77,10 +78,15 @@ func resourceDatasetCreate(ctx context.Context, d *schema.ResourceData, meta int
 func resourceDatasetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
 	client := meta.(*honeycombio.Client)
 
+	var detailedErr honeycombio.DetailedError
 	dataset, err := client.Datasets.Get(ctx, d.Id())
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
@@ -105,7 +111,7 @@ func resourceDatasetUpdate(ctx context.Context, d *schema.ResourceData, meta int
 	}
 	dataset, err := client.Datasets.Update(ctx, data)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(dataset.Slug)
diff --git a/honeycombio/resource_dataset_definition.go b/honeycombio/resource_dataset_definition.go
index 6356fb67..81511beb 100644
--- a/honeycombio/resource_dataset_definition.go
+++ b/honeycombio/resource_dataset_definition.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"strconv"
 
@@ -54,10 +55,16 @@ func resourceDatasetDefinitionRead(ctx context.Context, d *schema.ResourceData,
 	client := meta.(*honeycombio.Client)
 
 	dataset := d.Get("dataset").(string)
+
+	var detailedErr honeycombio.DetailedError
 	dd, err := client.DatasetDefinitions.Get(ctx, dataset)
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
@@ -83,7 +90,7 @@ func resourceDatasetDefinitionUpdate(ctx context.Context, d *schema.ResourceData
 	dd := expandDatasetDefinition(name, value)
 	_, err := client.DatasetDefinitions.Update(ctx, dataset, dd)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	return resourceDatasetDefinitionRead(ctx, d, meta)
@@ -99,7 +106,7 @@ func resourceDatasetDefinitionDelete(ctx context.Context, d *schema.ResourceData
 	dd := expandDatasetDefinition(name, "")
 	_, err := client.DatasetDefinitions.Update(ctx, dataset, dd)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	return nil
 }
diff --git a/honeycombio/resource_derived_column.go b/honeycombio/resource_derived_column.go
index 3660555e..25cd9b1c 100644
--- a/honeycombio/resource_derived_column.go
+++ b/honeycombio/resource_derived_column.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"strings"
 
@@ -69,10 +70,9 @@ func resourceDerivedColumnCreate(ctx context.Context, d *schema.ResourceData, me
 	dataset := d.Get("dataset").(string)
 	derivedColumn := readDerivedColumn(d)
 
-	var err error
-	derivedColumn, err = client.DerivedColumns.Create(ctx, dataset, derivedColumn)
+	derivedColumn, err := client.DerivedColumns.Create(ctx, dataset, derivedColumn)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.Set("alias", derivedColumn.Alias)
@@ -84,10 +84,15 @@ func resourceDerivedColumnRead(ctx context.Context, d *schema.ResourceData, meta
 
 	dataset := d.Get("dataset").(string)
 
+	var detailedErr honeycombio.DetailedError
 	derivedColumn, err := client.DerivedColumns.GetByAlias(ctx, dataset, d.Get("alias").(string))
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
@@ -107,7 +112,7 @@ func resourceDerivedColumnUpdate(ctx context.Context, d *schema.ResourceData, me
 
 	derivedColumn, err := client.DerivedColumns.Update(ctx, dataset, derivedColumn)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.Set("alias", derivedColumn.Alias)
@@ -121,7 +126,7 @@ func resourceDerivedColumnDelete(ctx context.Context, d *schema.ResourceData, me
 
 	err := client.DerivedColumns.Delete(ctx, dataset, d.Id())
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	return nil
 }
diff --git a/honeycombio/resource_marker.go b/honeycombio/resource_marker.go
index 4d78f865..7401e494 100644
--- a/honeycombio/resource_marker.go
+++ b/honeycombio/resource_marker.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -52,7 +53,7 @@ func resourceMarkerCreate(ctx context.Context, d *schema.ResourceData, meta inte
 	}
 	marker, err := client.Markers.Create(ctx, dataset, data)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(marker.ID)
@@ -62,10 +63,15 @@ func resourceMarkerCreate(ctx context.Context, d *schema.ResourceData, meta inte
 func resourceMarkerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
 	client := meta.(*honeycombio.Client)
 
+	var detailedErr honeycombio.DetailedError
 	marker, err := client.Markers.Get(ctx, d.Get("dataset").(string), d.Id())
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
diff --git a/honeycombio/resource_marker_setting.go b/honeycombio/resource_marker_setting.go
index e6f3bdf1..0950e573 100644
--- a/honeycombio/resource_marker_setting.go
+++ b/honeycombio/resource_marker_setting.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 	"regexp"
 	"time"
 
@@ -60,7 +61,7 @@ func resourceMarkerSettingCreate(ctx context.Context, d *schema.ResourceData, me
 	}
 	markerSetting, err := client.MarkerSettings.Create(ctx, dataset, data)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(markerSetting.ID)
@@ -72,10 +73,15 @@ func resourceMarkerSettingCreate(ctx context.Context, d *schema.ResourceData, me
 func resourceMarkerSettingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
 	client := meta.(*honeycombio.Client)
 
+	var detailedErr honeycombio.DetailedError
 	markerSetting, err := client.MarkerSettings.Get(ctx, d.Get("dataset").(string), d.Id())
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
@@ -101,7 +107,7 @@ func resourceMarkerSettingUpdate(ctx context.Context, d *schema.ResourceData, me
 	}
 	markerSetting, err := client.MarkerSettings.Update(ctx, dataset, data)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(markerSetting.ID)
@@ -115,7 +121,7 @@ func resourceMarkerSettingDelete(ctx context.Context, d *schema.ResourceData, me
 
 	err := client.MarkerSettings.Delete(ctx, dataset, d.Id())
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	return nil
 }
diff --git a/honeycombio/resource_query.go b/honeycombio/resource_query.go
index 15731c18..285b15eb 100644
--- a/honeycombio/resource_query.go
+++ b/honeycombio/resource_query.go
@@ -3,6 +3,7 @@ package honeycombio
 import (
 	"context"
 	"encoding/json"
+	"errors"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -47,7 +48,7 @@ func resourceQueryCreate(ctx context.Context, d *schema.ResourceData, meta inter
 
 	query, err := client.Queries.Create(ctx, dataset, &querySpec)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(*query.ID)
@@ -59,10 +60,15 @@ func resourceQueryRead(ctx context.Context, d *schema.ResourceData, meta interfa
 
 	dataset := d.Get("dataset").(string)
 
+	var detailedErr honeycombio.DetailedError
 	query, err := client.Queries.Get(ctx, dataset, d.Id())
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
diff --git a/honeycombio/resource_query_annotation.go b/honeycombio/resource_query_annotation.go
index be94d0be..8c59f413 100644
--- a/honeycombio/resource_query_annotation.go
+++ b/honeycombio/resource_query_annotation.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -58,7 +59,7 @@ func resourceQueryAnnotationCreate(ctx context.Context, d *schema.ResourceData,
 
 	queryAnnotation, err := client.QueryAnnotations.Create(ctx, dataset, queryAnnotation)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(queryAnnotation.ID)
@@ -72,7 +73,7 @@ func resourceQueryAnnotationUpdate(ctx context.Context, d *schema.ResourceData,
 
 	_, err := client.QueryAnnotations.Update(ctx, dataset, queryAnnotation)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	return resourceQueryAnnotationRead(ctx, d, meta)
@@ -84,7 +85,7 @@ func resourceQueryAnnotationDestroy(ctx context.Context, d *schema.ResourceData,
 
 	err := client.QueryAnnotations.Delete(ctx, dataset, d.Id())
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	return nil
 }
@@ -93,10 +94,15 @@ func resourceQueryAnnotationRead(ctx context.Context, d *schema.ResourceData, me
 	client := meta.(*honeycombio.Client)
 	dataset := d.Get("dataset").(string)
 
+	var detailedErr honeycombio.DetailedError
 	queryAnnotation, err := client.QueryAnnotations.Get(ctx, dataset, d.Id())
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
diff --git a/honeycombio/resource_slo.go b/honeycombio/resource_slo.go
index 5a66591e..0a7dc7d4 100644
--- a/honeycombio/resource_slo.go
+++ b/honeycombio/resource_slo.go
@@ -105,7 +105,8 @@ func resourceSLORead(ctx context.Context, d *schema.ResourceData, meta interface
 	dataset := d.Get("dataset").(string)
 
 	s, err := client.SLOs.Get(ctx, dataset, d.Id())
-	if err == honeycombio.ErrNotFound {
+	detailedErr, isDetailedErr := err.(*honeycombio.DetailedError)
+	if isDetailedErr && detailedErr.IsNotFound() {
 		d.SetId("")
 		return nil
 	} else if err != nil {
diff --git a/honeycombio/type_helpers.go b/honeycombio/type_helpers.go
index fa3d9a99..8d99b547 100644
--- a/honeycombio/type_helpers.go
+++ b/honeycombio/type_helpers.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"regexp"
 	"strconv"
@@ -289,7 +290,7 @@ func createRecipient(ctx context.Context, d *schema.ResourceData, meta interface
 
 	r, err = client.Recipients.Create(ctx, r)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(r.ID)
@@ -299,10 +300,15 @@ func createRecipient(ctx context.Context, d *schema.ResourceData, meta interface
 func readRecipient(ctx context.Context, d *schema.ResourceData, meta interface{}, t honeycombio.RecipientType) diag.Diagnostics {
 	client := meta.(*honeycombio.Client)
 
+	var detailedErr honeycombio.DetailedError
 	r, err := client.Recipients.Get(ctx, d.Id())
-	if err == honeycombio.ErrNotFound {
-		d.SetId("")
-		return nil
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			d.SetId("")
+			return nil
+		} else {
+			return diagFromDetailedErr(detailedErr)
+		}
 	} else if err != nil {
 		return diag.FromErr(err)
 	}
@@ -337,7 +343,7 @@ func updateRecipient(ctx context.Context, d *schema.ResourceData, meta interface
 
 	r, err = client.Recipients.Update(ctx, r)
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 
 	d.SetId(r.ID)
@@ -349,7 +355,7 @@ func deleteRecipient(ctx context.Context, d *schema.ResourceData, meta interface
 
 	err := client.Recipients.Delete(ctx, d.Id())
 	if err != nil {
-		return diag.FromErr(err)
+		return diagFromErr(err)
 	}
 	return nil
 }
@@ -426,3 +432,42 @@ func (f *recipientFilter) IsMatch(r honeycombio.Recipient) bool {
 
 	return true
 }
+
+// diagFromErr is a helper function that converts an error to a diag.Diagnostics.
+// Intended to be a drop-in replacement for diag.FromErr from the V2 Plugin SDK.
+//
+// If err is a honeycombio.DetailedError, a detailed Diagnostic will be added to diag,
+// otherwise a generic error Diagnostic will be added to diag.
+func diagFromErr(err error) diag.Diagnostics {
+	if err == nil {
+		return nil
+	}
+
+	var detailedErr honeycombio.DetailedError
+	if errors.As(err, &detailedErr) {
+		return diagFromDetailedErr(detailedErr)
+	}
+
+	return diag.FromErr(err)
+}
+
+func diagFromDetailedErr(err honeycombio.DetailedError) diag.Diagnostics {
+	diags := make(diag.Diagnostics, 0, len(err.Details)+1)
+	if len(err.Details) > 0 {
+		for _, d := range err.Details {
+			diags = append(diags, diag.Diagnostic{
+				Severity: diag.Error,
+				Summary:  err.Title,
+				Detail:   d.Code + " - " + d.Description,
+			})
+		}
+	} else {
+		diags = append(diags, diag.Diagnostic{
+			Severity: diag.Error,
+			Summary:  err.Title,
+			Detail:   err.Message,
+		})
+	}
+
+	return diags
+}
diff --git a/internal/helper/diag.go b/internal/helper/diag.go
new file mode 100644
index 00000000..65d9c311
--- /dev/null
+++ b/internal/helper/diag.go
@@ -0,0 +1,86 @@
+package helper
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+// DetailedErrorDiagnostic is a Diagnostic which nicely wraps a client.DetailedError
+type DetailedErrorDiagnostic struct {
+	summary string
+	e       *client.DetailedError
+}
+
+// compile-time check that DetailedErrorDiagnostic implements diag.Diagnostic
+var _ diag.Diagnostic = DetailedErrorDiagnostic{}
+
+// NewDetailedErrorDiagnostic creates a new DetailedErrorDiagnostic
+// taking a context-specific summary of the action and a DetailedError.
+func NewDetailedErrorDiagnostic(summary string, e *client.DetailedError) DetailedErrorDiagnostic {
+	return DetailedErrorDiagnostic{
+		e:       e,
+		summary: summary,
+	}
+}
+
+// AddDiagnosticOnError is a helper function which will take an error
+// and a context-specific summary of the action.
+//
+// If err is nil, this function will return false.
+// If err is non-nil, this function will return ture and take one the following actions:
+//   - if it is a client.DetailedError, a DetailedErrorDiagnostic will be added to diag.
+//   - otherwise a generic error diagnostic will be added to diag.
+func AddDiagnosticOnError(diag *diag.Diagnostics, summary string, err error) bool {
+	if err == nil {
+		return false
+	}
+
+	var detailedErr *client.DetailedError
+	if errors.As(err, &detailedErr) {
+		diag.Append(DetailedErrorDiagnostic{
+			summary: "Error " + summary,
+			e:       detailedErr,
+		})
+	} else {
+		diag.AddError("Error "+summary, err.Error())
+	}
+	return true
+}
+
+func (d DetailedErrorDiagnostic) Detail() string {
+	if len(d.e.Details) > 0 {
+		var response string
+		for i, dt := range d.e.Details {
+			response += dt.Code + " - " + dt.Description
+
+			if i > len(d.e.Details)-1 {
+				response += "\n "
+			}
+		}
+		return response
+	}
+
+	return d.e.Message
+}
+
+func (d DetailedErrorDiagnostic) Summary() string {
+	return fmt.Sprintf("%s (HTTP %d) - %s", d.summary, d.e.Status, d.e.Title)
+}
+
+func (d DetailedErrorDiagnostic) Severity() diag.Severity {
+	return diag.SeverityError
+}
+
+func (d DetailedErrorDiagnostic) Equal(other diag.Diagnostic) bool {
+	ed, ok := other.(DetailedErrorDiagnostic)
+	if !ok {
+		return false
+	}
+
+	return d.Summary() == ed.Summary() && reflect.DeepEqual(d, ed)
+}
diff --git a/internal/helper/ptr.go b/internal/helper/ptr.go
index fff9585f..be25edce 100644
--- a/internal/helper/ptr.go
+++ b/internal/helper/ptr.go
@@ -1,6 +1,6 @@
 package helper
 
-// Returns a pointer to the given value
+// ToPtr returns a pointer to the given value
 func ToPtr[T any](v T) *T {
 	return &v
 }
diff --git a/internal/helper/type_strings.go b/internal/helper/type_strings.go
index 30258e4f..d3e0eee3 100644
--- a/internal/helper/type_strings.go
+++ b/internal/helper/type_strings.go
@@ -1,25 +1,11 @@
 package helper
 
-import "github.com/honeycombio/terraform-provider-honeycombio/client"
-
-func TriggerThresholdOpStrings() []string {
-	in := client.TriggerThresholdOps()
-	out := make([]string, len(in))
-
-	for i := range in {
-		out[i] = string(in[i])
-	}
-
-	return out
-}
-
-func RecipientTypeStrings(recipientTypes []client.RecipientType) []string {
-	in := recipientTypes
-	out := make([]string, len(in))
-
+// AsStringSlice converts a slice of string-like things to a slice of strings.
+func AsStringSlice[S ~string](in []S) []string {
+	r := make([]string, len(in))
 	for i := range in {
-		out[i] = string(in[i])
+		r[i] = string(in[i])
 	}
 
-	return out
+	return r
 }
diff --git a/internal/provider/auth_metadata_data_source.go b/internal/provider/auth_metadata_data_source.go
index 10261b56..56315f8a 100644
--- a/internal/provider/auth_metadata_data_source.go
+++ b/internal/provider/auth_metadata_data_source.go
@@ -8,6 +8,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
 )
 
 // Ensure the implementation satisfies the expected interfaces.
@@ -167,10 +168,9 @@ func (d *authMetadataDataSource) Read(ctx context.Context, req datasource.ReadRe
 	var data authMetadataDataSourceModel
 
 	metadata, err := d.client.Auth.List(ctx)
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Unable to list Auth Metadata",
-			err.Error())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics,
+		"Listing Auth Metadata",
+		err) {
 		return
 	}
 
diff --git a/internal/provider/burn_alert_resource.go b/internal/provider/burn_alert_resource.go
index 763a4d79..32a5ab87 100644
--- a/internal/provider/burn_alert_resource.go
+++ b/internal/provider/burn_alert_resource.go
@@ -16,6 +16,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
 )
 
@@ -117,11 +118,7 @@ func (r *burnAlertResource) Create(ctx context.Context, req resource.CreateReque
 		SLO:               client.SLORef{ID: plan.SLOID.ValueString()},
 		Recipients:        expandNotificationRecipients(plan.Recipients),
 	})
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Creating Honeycomb Burn Alert",
-			"Could not create Burn Alert, unexpected error: "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Honeycomb Burn Alert", err) {
 		return
 	}
 
@@ -143,15 +140,24 @@ func (r *burnAlertResource) Read(ctx context.Context, req resource.ReadRequest,
 		return
 	}
 
+	var detailedErr *client.DetailedError
 	ba, err := r.client.BurnAlerts.Get(ctx, state.Dataset.ValueString(), state.ID.ValueString())
-	if errors.Is(err, client.ErrNotFound) {
-		resp.State.RemoveResource(ctx)
-		return
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			resp.State.RemoveResource(ctx)
+		} else {
+			resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic(
+				"Error Reading Honeycomb Burn Alert",
+				detailedErr,
+			))
+		}
 	} else if err != nil {
 		resp.Diagnostics.AddError(
 			"Error Reading Honeycomb Burn Alert",
 			"Could not read Burn Alert ID "+state.ID.ValueString()+": "+err.Error(),
 		)
+	}
+	if resp.Diagnostics.HasError() {
 		return
 	}
 
@@ -201,20 +207,12 @@ func (r *burnAlertResource) Update(ctx context.Context, req resource.UpdateReque
 		SLO:               client.SLORef{ID: plan.SLOID.ValueString()},
 		Recipients:        expandNotificationRecipients(plan.Recipients),
 	})
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Updating Honeycomb Burn Alert",
-			"Could not update Burn Alert, unexpected error: "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Burn Alert", err) {
 		return
 	}
 
 	burnAlert, err := r.client.BurnAlerts.Get(ctx, plan.Dataset.ValueString(), plan.ID.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Updating Honeycomb Burn Alert",
-			"Could not read Honeycomb Burn Alert ID "+plan.ID.ValueString()+": "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Burn Alert", err) {
 		return
 	}
 
@@ -237,11 +235,7 @@ func (r *burnAlertResource) Delete(ctx context.Context, req resource.DeleteReque
 	}
 
 	err := r.client.BurnAlerts.Delete(ctx, state.Dataset.ValueString(), state.ID.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Deleting Honeycomb Burn Alert",
-			"Could not delete Burn Alert, unexpected error: "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Deleting Honeycomb Burn Alert", err) {
 		return
 	}
 }
diff --git a/internal/provider/derived_column_data_source.go b/internal/provider/derived_column_data_source.go
index 1b67398c..0619c9d4 100644
--- a/internal/provider/derived_column_data_source.go
+++ b/internal/provider/derived_column_data_source.go
@@ -9,6 +9,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
 )
 
 // Ensure the implementation satisfies the expected interfaces.
@@ -82,10 +83,9 @@ func (d *derivedColumnDataSource) Read(ctx context.Context, req datasource.ReadR
 	}
 
 	dc, err := d.client.DerivedColumns.GetByAlias(ctx, data.Dataset.ValueString(), data.Alias.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError(
-			fmt.Sprintf("Unable to lookup Derived Column \"%s\"", data.Alias.ValueString()),
-			err.Error())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics,
+		fmt.Sprintf("Looking up Derived Column %q", data.ID.ValueString()),
+		err) {
 		return
 	}
 
diff --git a/internal/provider/derived_columns_data_source.go b/internal/provider/derived_columns_data_source.go
index 9b2c7bed..a68d50b3 100644
--- a/internal/provider/derived_columns_data_source.go
+++ b/internal/provider/derived_columns_data_source.go
@@ -9,6 +9,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/hashcode"
 )
 
@@ -77,8 +78,7 @@ func (d *derivedColumnsDataSource) Read(ctx context.Context, req datasource.Read
 	}
 
 	columns, err := d.client.DerivedColumns.List(ctx, data.Dataset.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError("Unable to list Derived Columns", err.Error())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Listing Derived Columns", err) {
 		return
 	}
 
diff --git a/internal/provider/notification_recipients.go b/internal/provider/notification_recipients.go
index f12017c0..50e72ac3 100644
--- a/internal/provider/notification_recipients.go
+++ b/internal/provider/notification_recipients.go
@@ -44,7 +44,7 @@ func notificationRecipientSchema() schema.SetNestedBlock {
 					Computed:    true,
 					Description: "The type of the trigger recipient.",
 					Validators: []validator.String{
-						stringvalidator.OneOf(helper.RecipientTypeStrings(client.TriggerRecipientTypes())...),
+						stringvalidator.OneOf(helper.AsStringSlice(client.TriggerRecipientTypes())...),
 					},
 				},
 				"target": schema.StringAttribute{
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 06184e3a..5cb89bbc 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -97,10 +97,10 @@ func (p *HoneycombioProvider) Configure(ctx context.Context, req provider.Config
 		)
 	}
 
-	apiKey := os.Getenv("HONEYCOMB_API_KEY")
+	apiKey := os.Getenv(client.DefaultAPIKeyEnv)
 	if apiKey == "" {
 		// fall through to legacy env var
-		apiKey = os.Getenv("HONEYCOMBIO_APIKEY")
+		apiKey = os.Getenv(client.LegacyAPIKeyEnv)
 	}
 	if !config.APIKey.IsNull() {
 		apiKey = config.APIKey.ValueString()
diff --git a/internal/provider/slo_data_source.go b/internal/provider/slo_data_source.go
index 278c6fbe..e49f692e 100644
--- a/internal/provider/slo_data_source.go
+++ b/internal/provider/slo_data_source.go
@@ -9,6 +9,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
 )
 
 // Ensure the implementation satisfies the expected interfaces.
@@ -99,10 +100,9 @@ func (d *sloDataSource) Read(ctx context.Context, req datasource.ReadRequest, re
 	}
 
 	slo, err := d.client.SLOs.Get(ctx, data.Dataset.ValueString(), data.ID.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError(
-			fmt.Sprintf("Unable to lookup SLO \"%s\"", data.ID.ValueString()),
-			err.Error())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics,
+		fmt.Sprintf("Looking up SLO %q", data.ID.ValueString()),
+		err) {
 		return
 	}
 
diff --git a/internal/provider/slos_data_source.go b/internal/provider/slos_data_source.go
index 35fb458c..7987907b 100644
--- a/internal/provider/slos_data_source.go
+++ b/internal/provider/slos_data_source.go
@@ -12,6 +12,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/filter"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/hashcode"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation"
@@ -121,8 +122,7 @@ func (d *slosDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
 	}
 
 	slos, err := d.client.SLOs.List(ctx, data.Dataset.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError("Unable to list SLOs", err.Error())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Listing SLOs", err) {
 		return
 	}
 
diff --git a/internal/provider/trigger_resource.go b/internal/provider/trigger_resource.go
index e1f712ac..bee6ad32 100644
--- a/internal/provider/trigger_resource.go
+++ b/internal/provider/trigger_resource.go
@@ -139,7 +139,7 @@ func (r *triggerResource) Schema(_ context.Context, _ resource.SchemaRequest, re
 							Required:    true,
 							Description: "The operator to apply.",
 							Validators: []validator.String{
-								stringvalidator.OneOf(helper.TriggerThresholdOpStrings()...),
+								stringvalidator.OneOf(helper.AsStringSlice(client.TriggerThresholdOps())...),
 							},
 						},
 						"value": schema.Float64Attribute{
@@ -228,11 +228,7 @@ func (r *triggerResource) Create(ctx context.Context, req resource.CreateRequest
 	}
 
 	trigger, err := r.client.Triggers.Create(ctx, plan.Dataset.ValueString(), newTrigger)
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Creating Honeycomb Trigger",
-			"Could not create Trigger, unexpected error: "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Honeycomb Trigger", err) {
 		return
 	}
 
@@ -260,15 +256,24 @@ func (r *triggerResource) Read(ctx context.Context, req resource.ReadRequest, re
 		return
 	}
 
+	var detailedErr *client.DetailedError
 	trigger, err := r.client.Triggers.Get(ctx, state.Dataset.ValueString(), state.ID.ValueString())
-	if errors.Is(err, client.ErrNotFound) {
-		resp.State.RemoveResource(ctx)
-		return
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			resp.State.RemoveResource(ctx)
+		} else {
+			resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic(
+				"Error Reading Honeycomb Trigger",
+				detailedErr,
+			))
+		}
 	} else if err != nil {
 		resp.Diagnostics.AddError(
 			"Error Reading Honeycomb Trigger",
-			"Could not read Trigger ID "+state.ID.ValueString()+": "+err.Error(),
+			"Unexpected error reading Trigger ID "+state.ID.ValueString()+": "+err.Error(),
 		)
+	}
+	if resp.Diagnostics.HasError() {
 		return
 	}
 
@@ -337,20 +342,12 @@ func (r *triggerResource) Update(ctx context.Context, req resource.UpdateRequest
 	}
 
 	_, err := r.client.Triggers.Update(ctx, plan.Dataset.ValueString(), updatedTrigger)
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Updating Honeycomb Trigger",
-			"Could not update Trigger, unexpected error: "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Trigger", err) {
 		return
 	}
 
 	trigger, err := r.client.Triggers.Get(ctx, plan.Dataset.ValueString(), plan.ID.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Updating Honeycomb Trigger",
-			"Could not read Honeycomb Trigger ID "+plan.ID.ValueString()+": "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Trigger", err) {
 		return
 	}
 
@@ -379,11 +376,7 @@ func (r *triggerResource) Delete(ctx context.Context, req resource.DeleteRequest
 	}
 
 	err := r.client.Triggers.Delete(ctx, state.Dataset.ValueString(), state.ID.ValueString())
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Error Deleting Honeycomb Trigger",
-			"Could not delete Trigger, unexpected error: "+err.Error(),
-		)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Deleting Honeycomb Trigger", err) {
 		return
 	}
 }


Test patch:
diff --git a/client/board_test.go b/client/board_test.go
index 5f35bd04..3c3c5fc3 100644
--- a/client/board_test.go
+++ b/client/board_test.go
@@ -104,9 +104,12 @@ func TestBoards(t *testing.T) {
 		assert.NoError(t, err)
 	})
 
-	t.Run("Get_deletedBoard", func(t *testing.T) {
+	t.Run("Fail to get deleted Board", func(t *testing.T) {
 		_, err := c.Boards.Get(ctx, b.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/burn_alert_test.go b/client/burn_alert_test.go
index 64830927..85d759de 100644
--- a/client/burn_alert_test.go
+++ b/client/burn_alert_test.go
@@ -96,9 +96,12 @@ func TestBurnAlerts(t *testing.T) {
 		assert.NoError(t, err, "failed to delete BurnAlert")
 	})
 
-	t.Run("Get_NotFound", func(t *testing.T) {
+	t.Run("Fail to Get deleted Burn Alert", func(t *testing.T) {
 		_, err := c.BurnAlerts.Get(ctx, dataset, burnAlert.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/client_test.go b/client/client_test.go
index 7431e404..e818cfd6 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -7,33 +7,35 @@ import (
 
 	"github.com/joho/godotenv"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func init() {
+func newTestClient(t *testing.T) *Client {
+	t.Helper()
+
 	// load environment values from a .env, if available
 	_ = godotenv.Load("../.env")
-}
 
-func newTestClient(t *testing.T) *Client {
-	apiKey, ok := os.LookupEnv("HONEYCOMB_API_KEY")
+	apiKey, ok := os.LookupEnv(DefaultAPIKeyEnv)
 	if !ok {
-		t.Fatal("expected environment variable HONEYCOMB_API_KEY")
+		t.Fatal("expected environment variable " + DefaultAPIKeyEnv)
 	}
-	_, debug := os.LookupEnv("DEBUG")
+	_, debug := os.LookupEnv("HONEYCOMBIO_DEBUG")
 
 	cfg := &Config{
-		APIKey: apiKey,
-		Debug:  debug,
+		APIKey:    apiKey,
+		Debug:     debug,
+		UserAgent: "go-honeycombio/test",
 	}
 	c, err := NewClient(cfg)
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err, "failed to create client")
 
 	return c
 }
 
 func testDataset(t *testing.T) string {
+	t.Helper()
+
 	dataset, ok := os.LookupEnv("HONEYCOMB_DATASET")
 	if !ok {
 		t.Fatalf("expected environment variable HONEYCOMB_DATASET")
@@ -42,54 +44,15 @@ func testDataset(t *testing.T) string {
 	return dataset
 }
 
-func TestConfig_merge(t *testing.T) {
-	config1 := &Config{
-		APIKey:    "",
-		APIUrl:    "",
-		UserAgent: "go-honeycombio",
-	}
-	config2 := &Config{
-		APIKey:    "",
-		APIUrl:    "http://localhost",
-		UserAgent: "",
-	}
-
-	expected := &Config{
-		APIKey:    "",
-		APIUrl:    "http://localhost",
-		UserAgent: "go-honeycombio",
-	}
-
-	config1.merge(config2)
-
-	assert.Equal(t, expected, config1)
-}
-
-func TestClient_invalidConfig(t *testing.T) {
-	cfg := &Config{
-		APIKey: "",
-	}
-	_, err := NewClient(cfg)
-	assert.Error(t, err, "APIKey must be configured")
-
-	cfg = &Config{
+func TestClient_InvalidConfig(t *testing.T) {
+	_, err := NewClient(&Config{
 		APIKey: "123",
 		APIUrl: "cache_object:foo/bar",
-	}
-	_, err = NewClient(cfg)
+	})
 	assert.Error(t, err)
 	assert.Contains(t, err.Error(), "could not parse APIUrl")
 }
 
-func TestClient_parseHoneycombioError(t *testing.T) {
-	ctx := context.Background()
-	c := newTestClient(t)
-
-	err := c.performRequest(ctx, "POST", "/1/boards/", nil, nil)
-
-	assert.Error(t, err, "400 Bad Request: request body should not be empty")
-}
-
 func TestClient_IsClassic(t *testing.T) {
 	ctx := context.Background()
 	c := newTestClient(t)
diff --git a/client/column_test.go b/client/column_test.go
index 51cf4ea2..b6c20365 100644
--- a/client/column_test.go
+++ b/client/column_test.go
@@ -91,9 +91,12 @@ func TestColumns(t *testing.T) {
 		assert.NoError(t, err)
 	})
 
-	t.Run("Get_notFound", func(t *testing.T) {
+	t.Run("Fail to get deleted Column", func(t *testing.T) {
 		_, err := c.Columns.Get(ctx, dataset, column.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/dataset_test.go b/client/dataset_test.go
index a9ddc70a..0d77306b 100644
--- a/client/dataset_test.go
+++ b/client/dataset_test.go
@@ -49,10 +49,13 @@ func TestDatasets(t *testing.T) {
 		assert.Equal(t, *currentDataset, *d)
 	})
 
-	t.Run("Get_notFound", func(t *testing.T) {
+	t.Run("Fail to Get bogus Dataset", func(t *testing.T) {
 		_, err := c.Datasets.Get(ctx, "does-not-exist")
 
-		assert.Equal(t, ErrNotFound, err)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 
 	t.Run("Create", func(t *testing.T) {
diff --git a/client/derived_column_test.go b/client/derived_column_test.go
index 5641b6fe..b180d688 100644
--- a/client/derived_column_test.go
+++ b/client/derived_column_test.go
@@ -2,9 +2,11 @@ package client
 
 import (
 	"context"
+	"net/http"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestDerivedColumns(t *testing.T) {
@@ -24,7 +26,7 @@ func TestDerivedColumns(t *testing.T) {
 		}
 		derivedColumn, err = c.DerivedColumns.Create(ctx, dataset, data)
 
-		assert.NoError(t, err)
+		require.NoError(t, err)
 
 		data.ID = derivedColumn.ID
 		assert.Equal(t, data, derivedColumn)
@@ -38,8 +40,10 @@ func TestDerivedColumns(t *testing.T) {
 		}
 		_, err = c.DerivedColumns.Create(ctx, dataset, data)
 
+		var de DetailedError
 		assert.Error(t, err)
-		assert.Contains(t, err.Error(), "chosen alias is the same as an existing derived column")
+		assert.ErrorAs(t, err, &de)
+		assert.Equal(t, de.Status, http.StatusConflict)
 	})
 
 	t.Run("List", func(t *testing.T) {
@@ -85,15 +89,17 @@ func TestDerivedColumns(t *testing.T) {
 		assert.NoError(t, err)
 	})
 
-	t.Run("Get_notFound", func(t *testing.T) {
+	t.Run("Fail to Get Deleted DC", func(t *testing.T) {
 		_, err := c.DerivedColumns.Get(ctx, dataset, derivedColumn.ID)
 
-		assert.Equal(t, ErrNotFound, err)
-	})
-
-	t.Run("GetByAlias_notFound", func(t *testing.T) {
-		_, err := c.DerivedColumns.GetByAlias(ctx, dataset, derivedColumn.Alias)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 
-		assert.Equal(t, ErrNotFound, err)
+		_, err = c.DerivedColumns.GetByAlias(ctx, dataset, derivedColumn.Alias)
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/errors_test.go b/client/errors_test.go
new file mode 100644
index 00000000..4f754d79
--- /dev/null
+++ b/client/errors_test.go
@@ -0,0 +1,36 @@
+package client
+
+import (
+	"context"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestClient_ParseDetailedError(t *testing.T) {
+	var de DetailedError
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	t.Run("Post with no body should fail with 400 unparseable", func(t *testing.T) {
+		err := c.performRequest(ctx, "POST", "/1/boards/", nil, nil)
+		require.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.Equal(t, de.Status, http.StatusBadRequest)
+		assert.Equal(t, de.Type, "https://api.honeycomb.io/problems/unparseable")
+		assert.Equal(t, de.Title, "The request body could not be parsed.")
+		assert.Equal(t, de.Message, "could not parse request body")
+	})
+
+	t.Run("Get into non-existent dataset should fail with 404 'Dataset not found'", func(t *testing.T) {
+		_, err := c.Markers.Get(ctx, "non-existent-dataset", "abcd1234")
+		require.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.Equal(t, de.Status, http.StatusNotFound)
+		assert.Equal(t, de.Type, "https://api.honeycomb.io/problems/not-found")
+		assert.Equal(t, de.Title, "The requested resource cannot be found.")
+		assert.Equal(t, de.Message, "Dataset not found")
+	})
+}
diff --git a/client/example_test.go b/client/example_test.go
deleted file mode 100644
index 12551efa..00000000
--- a/client/example_test.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package client
-
-import (
-	"context"
-	"fmt"
-	"os"
-)
-
-// This example shows how to configure and use a client to list all boards.
-func Example() {
-	apiKey, _ := os.LookupEnv("HONEYCOMB_API_KEY")
-
-	config := &Config{
-		APIKey: apiKey,
-	}
-	client, err := NewClient(config)
-	if err != nil {
-		panic(err)
-	}
-
-	ctx := context.Background()
-
-	boards, err := client.Boards.List(ctx)
-	if err != nil {
-		panic(err)
-	}
-
-	fmt.Printf("Found %d boards\n", len(boards))
-
-	for i, board := range boards {
-		fmt.Printf("%d| %s (%d queries)\n", i, board.Name, len(board.Queries))
-	}
-}
diff --git a/client/marker_settings_test.go b/client/marker_settings_test.go
index 68b5108b..07b682fd 100644
--- a/client/marker_settings_test.go
+++ b/client/marker_settings_test.go
@@ -71,9 +71,10 @@ func TestMarkerSettings(t *testing.T) {
 		assert.NoError(t, err)
 	})
 
-	t.Run("Get_deletedMarker", func(t *testing.T) {
+	t.Run("Fail to Get deleted Marker Setting", func(t *testing.T) {
 		_, err := c.MarkerSettings.Get(ctx, dataset, m.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		assert.Error(t, err)
+		assert.True(t, err.(*DetailedError).IsNotFound())
 	})
 }
diff --git a/client/marker_test.go b/client/marker_test.go
index 89d7985f..3eb3b0ce 100644
--- a/client/marker_test.go
+++ b/client/marker_test.go
@@ -76,9 +76,10 @@ func TestMarkers(t *testing.T) {
 		assert.NoError(t, err)
 	})
 
-	t.Run("Get_deletedMarker", func(t *testing.T) {
+	t.Run("Fail to Get deleted Marker", func(t *testing.T) {
 		_, err := c.Markers.Get(ctx, dataset, m.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		assert.Error(t, err)
+		assert.True(t, err.(*DetailedError).IsNotFound())
 	})
 }
diff --git a/client/query_annotation_test.go b/client/query_annotation_test.go
index 57ff26f0..a0953a5c 100644
--- a/client/query_annotation_test.go
+++ b/client/query_annotation_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestQueryAnnotations(t *testing.T) {
@@ -16,6 +17,7 @@ func TestQueryAnnotations(t *testing.T) {
 	var queryAnnotation *QueryAnnotation
 	var err error
 
+	// no cleanup func needed as queries cannot be deleted
 	query, err := c.Queries.Create(ctx, dataset, &QuerySpec{
 		Calculations: []CalculationSpec{
 			{
@@ -23,10 +25,7 @@ func TestQueryAnnotations(t *testing.T) {
 			},
 		},
 	})
-
-	if err != nil {
-		t.Fatal(err)
-	}
+	require.NoError(t, err)
 
 	t.Run("Create", func(t *testing.T) {
 		data := &QueryAnnotation{
@@ -36,10 +35,7 @@ func TestQueryAnnotations(t *testing.T) {
 		}
 		queryAnnotation, err = c.QueryAnnotations.Create(ctx, dataset, data)
 
-		if err != nil {
-			t.Fatal(err)
-		}
-
+		require.NoError(t, err)
 		data.ID = queryAnnotation.ID
 		assert.Equal(t, data, queryAnnotation)
 	})
@@ -47,20 +43,14 @@ func TestQueryAnnotations(t *testing.T) {
 	t.Run("List", func(t *testing.T) {
 		queryAnnotations, err := c.QueryAnnotations.List(ctx, dataset)
 
-		if err != nil {
-			t.Fatal(err)
-		}
-
+		assert.NoError(t, err)
 		assert.Contains(t, queryAnnotations, *queryAnnotation, "could not find QueryAnnotation with List")
 	})
 
 	t.Run("Get", func(t *testing.T) {
 		result, err := c.QueryAnnotations.Get(ctx, dataset, queryAnnotation.ID)
 
-		if err != nil {
-			t.Fatal(err)
-		}
-
+		assert.NoError(t, err)
 		assert.Equal(t, *queryAnnotation, *result)
 	})
 
@@ -74,25 +64,22 @@ func TestQueryAnnotations(t *testing.T) {
 		}
 		queryAnnotation, err = c.QueryAnnotations.Update(ctx, dataset, data)
 
-		if err != nil {
-			t.Fatal(err)
-		}
-
+		assert.NoError(t, err)
 		data.ID = queryAnnotation.ID
 		assert.Equal(t, data, queryAnnotation)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
 		err = c.QueryAnnotations.Delete(ctx, dataset, queryAnnotation.ID)
-
-		if err != nil {
-			t.Fatal(err)
-		}
+		require.NoError(t, err)
 	})
 
-	t.Run("Get_notFound", func(t *testing.T) {
+	t.Run("Fail to Get deleted Query Annotation", func(t *testing.T) {
 		_, err := c.QueryAnnotations.Get(ctx, dataset, queryAnnotation.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/query_result_test.go b/client/query_result_test.go
index 72722fab..19009e24 100644
--- a/client/query_result_test.go
+++ b/client/query_result_test.go
@@ -2,10 +2,10 @@ package client
 
 import (
 	"context"
-	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestQueryResults(t *testing.T) {
@@ -25,23 +25,21 @@ func TestQueryResults(t *testing.T) {
 		},
 		TimeRange: ToPtr(60 * 60 * 24),
 	})
+	require.NoError(t, err)
 
 	t.Run("Create", func(t *testing.T) {
 		result, err = c.QueryResults.Create(ctx, dataset, &QueryResultRequest{
 			ID: *query.ID,
 		})
 
-		assert.Nil(t, err, fmt.Sprintf("result errored: %v", err))
+		require.NoError(t, err)
 		assert.NotEmpty(t, result.ID, "result missing ID")
 	})
 
 	t.Run("Get", func(t *testing.T) {
 		err := c.QueryResults.Get(ctx, dataset, result)
 
-		if err != nil {
-			t.Fatal(err)
-		}
-
+		assert.NoError(t, err)
 		assert.True(t, result.Complete, "query result didn't complete")
 		assert.NotNil(t, result.Data.Series, "empty data series")
 		assert.NotNil(t, result.Data.Results, "empty data results")
@@ -49,9 +47,12 @@ func TestQueryResults(t *testing.T) {
 		assert.NotEmpty(t, result.Links.Url, "empty result Url")
 	})
 
-	t.Run("Get_notFound", func(t *testing.T) {
+	t.Run("Fail to Get bogus Query Result", func(t *testing.T) {
 		err := c.QueryResults.Get(ctx, dataset, &QueryResult{ID: "abcd1234"})
 
-		assert.ErrorIs(t, err, ErrNotFound)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/recipient_test.go b/client/recipient_test.go
index bcc3850b..e20029ae 100644
--- a/client/recipient_test.go
+++ b/client/recipient_test.go
@@ -65,8 +65,12 @@ func TestRecipientsEmail(t *testing.T) {
 		assert.NoError(t, err, "failed to delete Recipient")
 	})
 
-	t.Run("Get_NotFound", func(t *testing.T) {
+	t.Run("Fail to Get deleted Recipient", func(t *testing.T) {
 		_, err := c.Recipients.Get(ctx, rcpt.ID)
-		assert.Equal(t, ErrNotFound, err)
+
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/slo_test.go b/client/slo_test.go
index 42e7c2e6..79aa9b75 100644
--- a/client/slo_test.go
+++ b/client/slo_test.go
@@ -85,9 +85,12 @@ func TestSLOs(t *testing.T) {
 		assert.NoError(t, err, "failed to delete SLO")
 	})
 
-	t.Run("Get_NotFound", func(t *testing.T) {
+	t.Run("Fail to Get deleted SLO", func(t *testing.T) {
 		_, err := c.SLOs.Get(ctx, dataset, slo.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
diff --git a/client/trigger_test.go b/client/trigger_test.go
index 169558df..8f2c5e9f 100644
--- a/client/trigger_test.go
+++ b/client/trigger_test.go
@@ -127,10 +127,13 @@ func TestTriggers(t *testing.T) {
 		assert.NoError(t, err)
 	})
 
-	t.Run("Get_deletedTrigger", func(t *testing.T) {
+	t.Run("Fail to Get deleted Trigger", func(t *testing.T) {
 		_, err := c.Triggers.Get(ctx, dataset, trigger.ID)
 
-		assert.Equal(t, ErrNotFound, err)
+		var de DetailedError
+		assert.Error(t, err)
+		assert.ErrorAs(t, err, &de)
+		assert.True(t, de.IsNotFound())
 	})
 }
 
diff --git a/honeycombio/data_source_column_test.go b/honeycombio/data_source_column_test.go
index ca8238dd..689e7b08 100644
--- a/honeycombio/data_source_column_test.go
+++ b/honeycombio/data_source_column_test.go
@@ -53,7 +53,7 @@ func TestAccDataSourceHoneycombioColumn_basic(t *testing.T) {
 			// test a failed match
 			{
 				Config:      testAccDataSourceColumnConfig([]string{"dataset = \"" + testAccDataset() + "\"", "name = \"test_column5\""}),
-				ExpectError: regexp.MustCompile("Error: column not found, API returned: 404 Not Found"),
+				ExpectError: regexp.MustCompile("404 Not Found"),
 			},
 		},
 	})
diff --git a/honeycombio/provider_test.go b/honeycombio/provider_test.go
index 3b849147..4a474ddf 100644
--- a/honeycombio/provider_test.go
+++ b/honeycombio/provider_test.go
@@ -32,10 +32,7 @@ var testAccProviderFactories = map[string]func() (*schema.Provider, error){
 }
 
 func testAccClient(t *testing.T) *honeycombio.Client {
-	cfg := &honeycombio.Config{
-		APIKey: os.Getenv("HONEYCOMB_API_KEY"),
-	}
-	c, err := honeycombio.NewClient(cfg)
+	c, err := honeycombio.NewClient(honeycombio.DefaultConfig())
 	if err != nil {
 		t.Fatalf("could not initialize honeycombio.Client: %v", err)
 	}
diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go
index 3dcbd1fc..5ab6f5f4 100644
--- a/internal/provider/provider_test.go
+++ b/internal/provider/provider_test.go
@@ -63,10 +63,7 @@ func testAccDataset() string {
 }
 
 func testAccClient(t *testing.T) *client.Client {
-	cfg := &client.Config{
-		APIKey: os.Getenv("HONEYCOMB_API_KEY"),
-	}
-	c, err := client.NewClient(cfg)
+	c, err := client.NewClient(client.DefaultConfig())
 	if err != nil {
 		t.Fatalf("could not initialize Honeycomb client: %v", err)
 	}