profiles

package
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package profiles manages named Dash0 configuration profiles.

Profiles are stored in ~/.dash0/profiles.json as named sets of connection parameters (API URL, auth token, OTLP URL, dataset). One profile is marked as active in ~/.dash0/activeProfile and used as the default configuration. Environment variables (DASH0_API_URL, DASH0_AUTH_TOKEN, DASH0_OTLP_URL, DASH0_DATASET) override the active profile.

This package imports the root dash0 package for dash0.ClientOption and dash0.DatasetPtr -- never the reverse.

Index

Examples

Constants

View Source
const (
	// ConfigDirName is the name of the directory containing Dash0 configuration.
	ConfigDirName = ".dash0"
	// ProfilesFileName is the name of the file containing profile configurations.
	ProfilesFileName = "profiles.json"
	// ActiveProfileFileName is the name of the file containing the active profile name.
	ActiveProfileFileName = "activeProfile"

	// EnvApiUrl is the environment variable for the Dash0 API URL.
	EnvApiUrl = "DASH0_API_URL"
	// EnvAuthToken is the environment variable for the Dash0 auth token.
	EnvAuthToken = "DASH0_AUTH_TOKEN"
	// EnvOtlpUrl is the environment variable for the Dash0 OTLP URL.
	EnvOtlpUrl = "DASH0_OTLP_URL"
	// EnvDataset is the environment variable for the Dash0 dataset.
	EnvDataset = "DASH0_DATASET"
	// EnvConfigDir is the environment variable that overrides the default
	// configuration directory.
	EnvConfigDir = "DASH0_CONFIG_DIR"
)
View Source
const MaxProfileNameLength = 64

MaxProfileNameLength is the maximum allowed length of a profile name. The cap keeps profile names within filesystem-friendly bounds and prevents pathological inputs from inflating the activeProfile pointer file.

View Source
const OAuthClientsFileName = "oauth-clients.json"

OAuthClientsFileName is the file storing cached dynamic client registrations.

View Source
const OAuthRefreshExpiresInDefault = 1 * time.Hour

OAuthRefreshExpiresInDefault is the lifetime assumed when the IdP omits expires_in. RFC 6749 §5.1 makes expires_in RECOMMENDED but not required; rather than reject a compliant response that omits it, the library falls back to this conservative default, which is comfortably above OAuthRefreshMinExpiresIn so an immediate refresh storm does not follow.

View Source
const OAuthRefreshMaxExpiresIn = 24 * time.Hour

OAuthRefreshMaxExpiresIn caps the trusted lifetime of an OAuth access token. A token endpoint that returns expires_in beyond this ceiling is treated as untrustworthy and the response is rejected before the caller can adopt it.

View Source
const OAuthRefreshMinExpiresIn = 2 * OAuthRefreshThreshold

OAuthRefreshMinExpiresIn is the minimum trusted lifetime of a refreshed access token. Tokens shorter than this would cross OAuthRefreshThreshold almost immediately and trigger a per-minute refresh storm; the IdP either misconfigured the client or is unhealthy, and the safe response is to reject the rotation and surface a clear error. The 2× factor preserves a useful working window: at least one OAuthRefreshThreshold of "fresh" plus another of "still serviceable".

View Source
const OAuthRefreshThreshold = 5 * time.Minute

OAuthRefreshThreshold is how far before expiry a token refresh is attempted.

Variables

View Source
var (
	// ErrNoActiveProfile is returned when there is no active profile.
	ErrNoActiveProfile = errors.New("no active profile configured")
	// ErrProfileNotFound is returned when a requested profile is not found.
	ErrProfileNotFound = errors.New("profile not found")
)
View Source
var ErrOAuthClientsFileCorrupt = errors.New("oauth-clients.json is corrupt")

ErrOAuthClientsFileCorrupt is wrapped into the error returned by OAuthClientStore.Get when the on-disk file exists but is unparseable. OAuthClientStore.Put and OAuthClientStore.Delete handle corruption internally by quarantining the bad file (see [OAuthClientStore.load]), rather than failing.

View Source
var ErrReauthenticationRequired = errors.New("OAuth refresh token rejected; re-authentication required")

ErrReauthenticationRequired is returned by refresh-bearing operations when the OAuth refresh token is no longer accepted by the authorization server (RFC 6749 invalid_grant) and the caller must initiate a fresh interactive login. Callers should treat this as a terminal state for the affected profile: the stored refresh token has been cleared from disk before this error is returned, so a subsequent call no longer retries the dead credential.

View Source
var ErrRevocationFailed = errors.New("OAuth refresh token revocation failed; the token may still be valid on the authorization server")

ErrRevocationFailed is wrapped into the result of Store.RemoveProfile (and equivalents) when the local profile was successfully removed but the best-effort revocation of the OAuth refresh token failed. The refresh token may still be live on the authorization server until its natural expiry. Callers should detect this via errors.Is and surface a "revoke manually" hint to the user.

Functions

func CanonicalAPIURL added in v1.15.0

func CanonicalAPIURL(raw string) (string, error)

CanonicalAPIURL normalises an API URL into the key form used by OAuthClientStore. It lowercases the scheme and host, strips userinfo (so credentials never reach the on-disk key), drops the port when it matches the scheme default (so "https://api.example.com" and "https://api.example.com:443" share one cache entry), normalises the path with path.Clean, trims a trailing slash from the path, and drops query and fragment components. Returns an error when the input is not a parseable absolute URL.

Example
package main

import (
	"fmt"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	key, err := profiles.CanonicalAPIURL("HTTPS://API.EU-WEST-1.AWS.DASH0.COM/?x=1#frag")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(key)
}
Output:
https://api.eu-west-1.aws.dash0.com

func WithConfiguration

func WithConfiguration(ctx context.Context, cfg *Configuration) context.Context

WithConfiguration returns a new context with the given configuration stored.

Example
package main

import (
	"context"
	"fmt"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	cfg := &profiles.Configuration{
		ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
		AuthToken: "auth_example-token",
		Dataset:   "production",
	}

	ctx := profiles.WithConfiguration(context.Background(), cfg)

	retrieved := profiles.FromContext(ctx)
	fmt.Println(retrieved.ApiUrl)

}
Output:
https://api.eu-west-1.aws.dash0.com

Types

type Configuration

type Configuration struct {
	ApiUrl    string `json:"apiUrl"`
	AuthToken string `json:"authToken"`
	OtlpUrl   string `json:"otlpUrl,omitempty"`
	Dataset   string `json:"dataset,omitempty"`

	// OAuth is set when the profile should authorize using an OAuth access token.
	// Potentially refreshed when close to the expiry.
	// The actual OAuth access token is located within the AuthToken field.
	OAuth *OAuthState `json:"oauth,omitempty"`
}

Configuration represents a Dash0 configuration with connection parameters.

func FromContext

func FromContext(ctx context.Context) *Configuration

FromContext retrieves the configuration from ctx, or nil if not present.

func ResolveConfiguration

func ResolveConfiguration(apiUrl, authToken string, opts ...StoreOption) (*Configuration, error)

ResolveConfiguration loads the active profile, applies environment variable overrides, then applies the given parameter overrides on top. Non-empty parameters take highest precedence.

This is a convenience function that creates a temporary Store internally. Pass StoreOption values (e.g. WithConfigDir) to control how the profile store is constructed.

OAuth refresh, if needed, uses context.Background; use ResolveConfigurationContext to plumb a cancellable context.

Example (ParameterOverride)
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	// Parameters take highest precedence, overriding both profiles and env vars.
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})

	cfg, err := profiles.ResolveConfiguration(
		"https://api.us-west-2.aws.dash0.com", // overrides profile's API URL
		"",                                    // falls back to profile's auth token
		profiles.WithConfigDir(configDir),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cfg.ApiUrl)
	fmt.Println(cfg.AuthToken)

}
Output:
https://api.us-west-2.aws.dash0.com
auth_dev-token

func ResolveConfigurationContext added in v1.15.0

func ResolveConfigurationContext(ctx context.Context, apiUrl, authToken string, opts ...StoreOption) (*Configuration, error)

ResolveConfigurationContext is the context-aware variant of ResolveConfiguration.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	cfg, err := profiles.ResolveConfigurationContext(ctx, "", "",
		profiles.WithConfigDir(configDir),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cfg.ApiUrl)
}
Output:
https://api.eu-west-1.aws.dash0.com

func ResolveConfigurationWithOtlp

func ResolveConfigurationWithOtlp(apiUrl, authToken, otlpUrl, dataset string, opts ...StoreOption) (*Configuration, error)

ResolveConfigurationWithOtlp is like ResolveConfiguration but also accepts OTLP URL and dataset overrides.

OAuth refresh, if needed, uses context.Background; use ResolveConfigurationWithOtlpContext to plumb a cancellable context.

func ResolveConfigurationWithOtlpContext added in v1.15.0

func ResolveConfigurationWithOtlpContext(ctx context.Context, apiUrl, authToken, otlpUrl, dataset string, opts ...StoreOption) (*Configuration, error)

ResolveConfigurationWithOtlpContext is the context-aware variant of ResolveConfigurationWithOtlp.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	cfg, err := profiles.ResolveConfigurationWithOtlpContext(ctx,
		"", "",
		"https://ingress.eu-west-1.aws.dash0.com",
		"production",
		profiles.WithConfigDir(configDir),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cfg.OtlpUrl, cfg.Dataset)
}
Output:
https://ingress.eu-west-1.aws.dash0.com production

func (*Configuration) ClientOptions

func (cfg *Configuration) ClientOptions() []dash0.ClientOption

ClientOptions returns dash0.ClientOption values that configure a client from this Configuration. Non-empty fields are mapped as follows:

The Dataset field is not mapped because it is a per-request parameter, not a client-level setting. Use Configuration.DatasetPtr to convert it for API calls.

Callers can append additional options to override or supplement the returned slice:

opts := cfg.ClientOptions()
opts = append(opts, dash0.WithUserAgent("my-tool/1.0"))
client, err := dash0.NewClient(opts...)
Example
package main

import (
	"fmt"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	cfg := &profiles.Configuration{
		ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
		AuthToken: "auth_example-token",
	}

	opts := cfg.ClientOptions()
	fmt.Printf("produced %d client options\n", len(opts))

}
Output:
produced 2 client options

func (*Configuration) DatasetPtr

func (cfg *Configuration) DatasetPtr() *string

DatasetPtr returns the dataset as a *string suitable for Dash0 API calls. It returns nil for empty strings and "default", matching dash0.DatasetPtr.

Example
package main

import (
	"fmt"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	cfg := &profiles.Configuration{Dataset: "production"}
	ptr := cfg.DatasetPtr()
	fmt.Println(*ptr)

	cfgDefault := &profiles.Configuration{Dataset: "default"}
	ptrDefault := cfgDefault.DatasetPtr()
	fmt.Println(ptrDefault)

}
Output:
production
<nil>

type OAuthClientRecord added in v1.15.0

type OAuthClientRecord struct {
	ClientID                string `json:"clientId"`
	RegistrationAccessToken string `json:"registrationAccessToken,omitempty"`
	RedirectURI             string `json:"redirectUri"`
}

OAuthClientRecord is a cached dynamic client registration (RFC 7591).

type OAuthClientStore added in v1.15.0

type OAuthClientStore struct {
	// contains filtered or unexported fields
}

OAuthClientStore caches dynamic client registrations keyed by canonical API URL. The file is created mode 0600 because RegistrationAccessToken (RFC 7592) is a long-lived management credential.

In-process mutation (Get/Put/Delete) is serialized by mu; cross-process mutation is serialized via an OS-level advisory lock on [oauthClientsLockFileName] so two CLI invocations sharing the same config directory cannot lose-update each other's RegistrationAccessToken values.

func NewOAuthClientStore added in v1.15.0

func NewOAuthClientStore(opts ...StoreOption) (*OAuthClientStore, error)

NewOAuthClientStore mirrors NewStore: explicit WithConfigDir > DASH0_CONFIG_DIR > ~/.dash0/. Same best-effort hygiene as NewStore: best-effort dir-mode tightening and stale-tempfile cleanup when the directory already exists.

Example
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, err := profiles.NewOAuthClientStore(profiles.WithConfigDir(configDir))
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	_, found, err := store.Get("https://api.eu-west-1.aws.dash0.com")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("found:", found)
}
Output:
found: false

func (*OAuthClientStore) Delete added in v1.15.0

func (s *OAuthClientStore) Delete(apiURL string) error

Delete removes the record for apiURL. A miss is a no-op. Cross-process serialization mirrors OAuthClientStore.Put.

Example
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewOAuthClientStore(profiles.WithConfigDir(configDir))
	_ = store.Put("https://api.eu-west-1.aws.dash0.com", profiles.OAuthClientRecord{
		ClientID:    "client-123",
		RedirectURI: "http://localhost:8080/callback",
	})

	if err := store.Delete("https://api.eu-west-1.aws.dash0.com"); err != nil {
		fmt.Println("error:", err)
		return
	}
	_, found, _ := store.Get("https://api.eu-west-1.aws.dash0.com")
	fmt.Println("found after delete:", found)
}
Output:
found after delete: false

func (*OAuthClientStore) Get added in v1.15.0

func (s *OAuthClientStore) Get(apiURL string) (OAuthClientRecord, bool, error)

Get returns the cached record for apiURL, or (zero, false, nil) on miss. A missing file is a miss, not an error. A corrupt file returns ErrOAuthClientsFileCorrupt wrapped with the quarantine path so the operator can decide whether to investigate.

Example
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewOAuthClientStore(profiles.WithConfigDir(configDir))
	_ = store.Put("https://api.eu-west-1.aws.dash0.com", profiles.OAuthClientRecord{
		ClientID:    "client-123",
		RedirectURI: "http://localhost:8080/callback",
	})

	rec, found, err := store.Get("https://api.eu-west-1.aws.dash0.com")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(found, rec.ClientID)
}
Output:
true client-123

func (*OAuthClientStore) Put added in v1.15.0

func (s *OAuthClientStore) Put(apiURL string, rec OAuthClientRecord) error

Put upserts a record for apiURL. The read-modify-write sequence is guarded by both an in-process mutex and a cross-process advisory lock on [oauthClientsLockFileName] so two CLI invocations sharing the same config directory cannot lose-update each other's RegistrationAccessToken values. If the on-disk file is corrupt it is quarantined (renamed with a timestamp suffix) before the new record is written; the previous registrations are lost from the active file but preserved on disk for manual recovery.

Example
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, err := profiles.NewOAuthClientStore(profiles.WithConfigDir(configDir))
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	if err := store.Put("https://api.eu-west-1.aws.dash0.com", profiles.OAuthClientRecord{
		ClientID:    "client-123",
		RedirectURI: "http://localhost:8080/callback",
	}); err != nil {
		fmt.Println("error:", err)
		return
	}
	rec, found, _ := store.Get("https://api.eu-west-1.aws.dash0.com")
	fmt.Println(found, rec.ClientID)
}
Output:
true client-123

type OAuthState added in v1.15.0

type OAuthState struct {
	ClientID     string    `json:"clientId,omitempty"`
	RefreshToken string    `json:"refreshToken,omitempty"`
	ExpiresAt    time.Time `json:"expiresAt,omitzero"`
}

OAuthState carries the OAuth-specific state that must survive across CLI invocations: the dynamic client identifier issued at registration (RFC 7591), the long-lived refresh token used to mint new access tokens, and the access token's expiry deadline. The current access token lives in Configuration.AuthToken; OAuthState only describes how to refresh it.

type Profile

type Profile struct {
	Name          string        `json:"name"`
	Configuration Configuration `json:"configuration"`
}

Profile represents a named configuration profile.

type ProfilesFile

type ProfilesFile struct {
	Profiles []Profile `json:"profiles"`
}

ProfilesFile represents the file storing multiple profiles.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store handles profile storage and retrieval.

Cross-process refresh: Store serializes OAuth token refreshes both within a process (via an in-memory mutex) and across processes that share the same config directory (via an OS-level advisory lock on .profile-lock, backed by github.com/gofrs/flock).

func NewStore

func NewStore(opts ...StoreOption) (*Store, error)

NewStore creates a new profile store.

The configuration directory is resolved in this order:

  1. Explicit WithConfigDir option (if provided).
  2. The DASH0_CONFIG_DIR environment variable (if set).
  3. ~/.dash0/ (default).

Best-effort hygiene on the resolved directory: the mode is tightened to [configDirMode] (0700) when broader, and any leftover writeFileAtomic temp files older than [staleTempFileAge] are removed. SIGKILL or a panic mid-write skips the deferred cleanup in writeFileAtomic, so .tmp-* files can accumulate. Both steps tolerate a missing directory (it will be created later by writeFileAtomic when the first write happens).

Example
package main

import (
	"fmt"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	svc, err := profiles.NewStore()
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	allProfiles, err := svc.GetProfiles()
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("found %d profiles\n", len(allProfiles))
}
Example (WithConfigDir)
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	svc, err := profiles.NewStore(profiles.WithConfigDir(configDir))
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	allProfiles, err := svc.GetProfiles()
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("found %d profiles\n", len(allProfiles))

}
Output:
found 0 profiles

func (*Store) AddProfile

func (s *Store) AddProfile(profile Profile) error

AddProfile adds a new profile to the configuration. If a profile with the same name already exists, it is replaced. When adding the first profile, it is automatically set as active.

The read-modify-write sequence is serialized cross-process via the .profile-lock sentinel; see Store for the locking model. Lock acquisition uses context.Background; callers that need to bound the wait should use Store.AddProfileContext.

func (*Store) AddProfileContext added in v1.15.0

func (s *Store) AddProfileContext(ctx context.Context, profile Profile) error

AddProfileContext is the context-aware variant of Store.AddProfile. The ctx bounds the wait to acquire the cross-process [.profile-lock]. Returns an error from [validateProfileName] when the profile name is empty, contains control characters or path separators, starts with '.', or exceeds MaxProfileNameLength.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))

	// The Context variant lets the caller bound the wait for the
	// cross-process .profile-lock acquisition — useful when a long-running
	// agent flow already has a deadline context to honor.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	err := store.AddProfileContext(ctx, profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	all, _ := store.GetProfiles()
	fmt.Println("count:", len(all))
}
Output:
count: 1

func (*Store) GetActiveConfiguration

func (s *Store) GetActiveConfiguration() (*Configuration, error)

GetActiveConfiguration returns the currently active configuration. Environment variables take precedence over the active profile. If the active profile uses OAuth, the access token is refreshed when close to expiry, using context.Background for the refresh request. Use Store.GetActiveConfigurationContext to plumb a cancellable context through the refresh.

Example
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	// Create a temporary config directory with a profile.
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
			Dataset:   "staging",
		},
	})

	cfg, err := store.GetActiveConfiguration()
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cfg.ApiUrl)
	fmt.Println(cfg.Dataset)

}
Output:
https://api.eu-west-1.aws.dash0.com
staging
Example (EnvVarOverride)
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	// Environment variables override values from the active profile.
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
			Dataset:   "staging",
		},
	})

	// DASH0_DATASET overrides the profile's dataset.
	_ = os.Setenv(profiles.EnvDataset, "production")
	defer func() { _ = os.Unsetenv(profiles.EnvDataset) }()

	cfg, err := store.GetActiveConfiguration()
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cfg.ApiUrl)
	fmt.Println(cfg.Dataset)

}
Output:
https://api.eu-west-1.aws.dash0.com
production

func (*Store) GetActiveConfigurationContext added in v1.15.0

func (s *Store) GetActiveConfigurationContext(ctx context.Context) (*Configuration, error)

GetActiveConfigurationContext is the context-aware variant of Store.GetActiveConfiguration. The ctx is propagated through the OAuth refresh round-trip so a hung authorization server does not pin the caller for the full HTTP timeout.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	// The Context variant plumbs cancellation through the OAuth refresh
	// round-trip so a hung authorization server cannot pin the caller for
	// the full HTTP timeout.
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	cfg, err := store.GetActiveConfigurationContext(ctx)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cfg.ApiUrl)

}
Output:
https://api.eu-west-1.aws.dash0.com

func (*Store) GetActiveProfile

func (s *Store) GetActiveProfile() (*Profile, error)

GetActiveProfile returns the currently active profile.

func (*Store) GetProfiles

func (s *Store) GetProfiles() ([]Profile, error)

GetProfiles returns all available profiles.

func (*Store) RemoveProfile

func (s *Store) RemoveProfile(profileName string) error

RemoveProfile removes a profile from the configuration. If the profile has OAuth state, the refresh token is revoked before removal, using context.Background for the revoke request. Use Store.RemoveProfileContext to plumb a cancellable context.

Revocation is best-effort: if it fails, the local profile is still removed and the returned error wraps ErrRevocationFailed. Callers can detect this with errors.Is and surface a "revoke manually" hint -- the refresh token may still be live on the authorization server.

If the removed profile was the active profile, the first remaining profile becomes active.

Example
package main

import (
	"fmt"
	"os"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})

	if err := store.RemoveProfile("dev"); err != nil {
		fmt.Println("error:", err)
		return
	}
	all, _ := store.GetProfiles()
	fmt.Println("remaining:", len(all))
}
Output:
remaining: 0

func (*Store) RemoveProfileContext added in v1.15.0

func (s *Store) RemoveProfileContext(ctx context.Context, profileName string) error

RemoveProfileContext is the context-aware variant of Store.RemoveProfile.

The token-revocation HTTP round-trip runs AFTER the in-process and cross-process locks are released, so a hung authorization server cannot pin sibling CLI invocations sharing the same config directory. The cost is that the locks no longer cover the revocation; an interleaved process observing the now-removed profile will not see the revoke in-flight. That is acceptable because the refresh token is already gone from disk before revocation begins; the worst-case server-side residue is the unrevoked refresh token until its natural expiry.

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})

	// The Context variant plumbs cancellation through the OAuth refresh-token
	// revocation HTTP call, so the caller can give up on a slow IdP.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := store.RemoveProfileContext(ctx, "dev"); err != nil {
		fmt.Println("error:", err)
		return
	}
	all, _ := store.GetProfiles()
	fmt.Println("remaining:", len(all))
}
Output:
remaining: 0

func (*Store) SetActiveProfile

func (s *Store) SetActiveProfile(profileName string) error

SetActiveProfile sets the active profile by name. Returns ErrProfileNotFound if no profile with the given name exists.

The change is serialized cross-process via the .profile-lock sentinel; see Store for the locking model. Lock acquisition uses context.Background; callers that need to bound the wait should use Store.SetActiveProfileContext.

func (*Store) SetActiveProfileContext added in v1.15.0

func (s *Store) SetActiveProfileContext(ctx context.Context, profileName string) error

SetActiveProfileContext is the context-aware variant of Store.SetActiveProfile. The ctx bounds the wait to acquire the cross-process [.profile-lock].

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})
	_ = store.AddProfile(profiles.Profile{
		Name: "prod",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.us-west-2.aws.dash0.com",
			AuthToken: "auth_prod-token",
		},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := store.SetActiveProfileContext(ctx, "prod"); err != nil {
		fmt.Println("error:", err)
		return
	}
	active, _ := store.GetActiveProfile()
	fmt.Println(active.Name)
}
Output:
prod

func (*Store) UpdateProfile

func (s *Store) UpdateProfile(name string, updateFn func(*Configuration)) error

UpdateProfile finds a profile by name and applies updateFn to its configuration, then saves. Returns an error if no profile with the given name exists.

The read-modify-write sequence is serialized cross-process via the .profile-lock sentinel; see Store for the locking model. Lock acquisition uses context.Background; callers that need to bound the wait should use Store.UpdateProfileContext.

func (*Store) UpdateProfileContext added in v1.15.0

func (s *Store) UpdateProfileContext(ctx context.Context, name string, updateFn func(*Configuration)) error

UpdateProfileContext is the context-aware variant of Store.UpdateProfile. The ctx bounds the wait to acquire the cross-process [.profile-lock].

Example
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/dash0hq/dash0-api-client-go/profiles"
)

func main() {
	configDir, _ := os.MkdirTemp("", "dash0-example-*")
	defer func() { _ = os.RemoveAll(configDir) }()

	store, _ := profiles.NewStore(profiles.WithConfigDir(configDir))
	_ = store.AddProfile(profiles.Profile{
		Name: "dev",
		Configuration: profiles.Configuration{
			ApiUrl:    "https://api.eu-west-1.aws.dash0.com",
			AuthToken: "auth_dev-token",
		},
	})

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := store.UpdateProfileContext(ctx, "dev", func(cfg *profiles.Configuration) {
		cfg.Dataset = "production"
	}); err != nil {
		fmt.Println("error:", err)
		return
	}

	all, _ := store.GetProfiles()
	fmt.Println(all[0].Configuration.Dataset)
}
Output:
production

type StoreOption

type StoreOption func(*storeConfig)

StoreOption configures a Store.

func WithConfigDir

func WithConfigDir(dir string) StoreOption

WithConfigDir overrides the default configuration directory (~/.dash0/). This is useful for testing or for applications that store profiles in a non-standard location.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL