3 OpenFeature Provider
Insidious Fiddler edited this page 2026-02-06 21:04:12 +00:00

OpenFeature Provider

Use Hoist with the OpenFeature SDK for vendor-neutral feature flag evaluation.

Overview

The Hoist OpenFeature provider wraps the native Hoist SDK and implements the OpenFeature FeatureProvider interface. This allows you to:

  • Use the standard OpenFeature API
  • Switch providers without code changes
  • Leverage OpenFeature hooks and middleware

Installation

go get git.macco.dev/macco/hoist/sdks/go/openfeature
go get github.com/open-feature/go-sdk/openfeature

Quick Start

package main

import (
    "context"
    "log"

    "git.macco.dev/macco/hoist/sdks/go/hoist"
    hoistof "git.macco.dev/macco/hoist/sdks/go/openfeature"
    of "github.com/open-feature/go-sdk/openfeature"
)

func main() {
    // Create the Hoist provider
    provider, err := hoistof.NewProvider(hoist.Config{
        APIKey:  "hoist_live_xxx",
        BaseURL: "https://hoist.example.com",
    })
    if err != nil {
        log.Fatal(err)
    }
    defer provider.Close()

    // Register with OpenFeature
    of.SetProvider(provider)

    // Create a client
    client := of.NewClient("my-app")

    // Evaluate flags using standard OpenFeature API
    ctx := context.Background()
    enabled, err := client.BooleanValue(ctx, "dark-mode", false, of.EvaluationContext{
        TargetingKey: "user-123",
        Attributes: map[string]any{
            "plan": "pro",
        },
    })
    if err != nil {
        log.Printf("evaluation error: %v", err)
    }

    if enabled {
        // Dark mode enabled
    }
}

Provider Configuration

The provider accepts the same configuration as the native Hoist client:

provider, err := hoistof.NewProvider(hoist.Config{
    // Required
    APIKey:  "hoist_live_xxx",
    BaseURL: "https://hoist.example.com",

    // Optional
    PollInterval: 30 * time.Second,
    HTTPClient:   customHTTPClient,
    Logger:       customLogger,

    // Callbacks
    OnReady: func() {
        log.Println("Provider ready")
    },
    OnError: func(err error) {
        log.Printf("Provider error: %v", err)
    },
})

Evaluation Context Mapping

OpenFeature context maps to Hoist context:

OpenFeature Hoist
TargetingKey TargetingKey
Attributes["*"] Attributes["*"]
// OpenFeature context
ofCtx := of.EvaluationContext{
    TargetingKey: "user-123",
    Attributes: map[string]any{
        "email": "user@example.com",
        "plan":  "enterprise",
    },
}

// Automatically converted to Hoist context:
// hoist.Context{
//     TargetingKey: "user-123",
//     Attributes: map[string]any{
//         "email": "user@example.com",
//         "plan":  "enterprise",
//     },
// }

Evaluation Methods

Boolean

value, err := client.BooleanValue(ctx, "feature-flag", false, evalCtx)

// With details
details, err := client.BooleanValueDetails(ctx, "feature-flag", false, evalCtx)
fmt.Printf("Value: %v, Reason: %s, Variant: %s\n",
    details.Value, details.Reason, details.Variant)

String

value, err := client.StringValue(ctx, "string-flag", "default", evalCtx)

// With details
details, err := client.StringValueDetails(ctx, "string-flag", "default", evalCtx)

Integer

value, err := client.IntValue(ctx, "int-flag", 100, evalCtx)

// With details
details, err := client.IntValueDetails(ctx, "int-flag", 100, evalCtx)

Float

value, err := client.FloatValue(ctx, "float-flag", 0.5, evalCtx)

// With details
details, err := client.FloatValueDetails(ctx, "float-flag", 0.5, evalCtx)

Object (JSON)

value, err := client.ObjectValue(ctx, "json-flag", map[string]any{"default": true}, evalCtx)

// With details
details, err := client.ObjectValueDetails(ctx, "json-flag", map[string]any{"default": true}, evalCtx)

Reason Mapping

Hoist reasons are mapped to OpenFeature reasons:

Hoist Reason OpenFeature Reason
ReasonDefault DefaultReason
ReasonTargetingMatch TargetingMatchReason
ReasonSegmentMatch TargetingMatchReason
ReasonRollout TargetingMatchReason
ReasonIdentityOverride TargetingMatchReason
ReasonDisabled DisabledReason
ReasonNotFound ErrorReason
ReasonError ErrorReason

Flag Metadata

Evaluation details include Hoist-specific metadata:

details, _ := client.BooleanValueDetails(ctx, "feature", false, evalCtx)

// Access metadata
ruleID := details.FlagMetadata["ruleId"]
segmentKey := details.FlagMetadata["segmentKey"]
version := details.FlagMetadata["version"]

OpenFeature Hooks

Use OpenFeature hooks for cross-cutting concerns:

// Logging hook
type LoggingHook struct{}

func (h LoggingHook) Before(ctx context.Context, hookCtx of.HookContext, hints of.HookHints) (*of.EvaluationContext, error) {
    log.Printf("Evaluating: %s", hookCtx.FlagKey())
    return nil, nil
}

func (h LoggingHook) After(ctx context.Context, hookCtx of.HookContext, details of.InterfaceEvaluationDetails, hints of.HookHints) error {
    log.Printf("Result: %s = %v (reason: %s)", hookCtx.FlagKey(), details.Value, details.Reason)
    return nil
}

func (h LoggingHook) Error(ctx context.Context, hookCtx of.HookContext, err error, hints of.HookHints) {
    log.Printf("Error: %s: %v", hookCtx.FlagKey(), err)
}

func (h LoggingHook) Finally(ctx context.Context, hookCtx of.HookContext, hints of.HookHints) {
    // Cleanup
}

// Register the hook
of.AddHooks(LoggingHook{})

Multiple Providers

Use named providers for different services:

// Production flags
prodProvider, _ := hoistof.NewProvider(hoist.Config{
    APIKey:  "hoist_live_prod",
    BaseURL: "https://hoist.example.com",
})
of.SetNamedProvider("production", prodProvider)

// Experiment flags
expProvider, _ := hoistof.NewProvider(hoist.Config{
    APIKey:  "hoist_live_exp",
    BaseURL: "https://hoist.example.com",
})
of.SetNamedProvider("experiments", expProvider)

// Use specific provider
prodClient := of.NewClient("my-app", of.WithProvider("production"))
expClient := of.NewClient("my-app", of.WithProvider("experiments"))

Provider Lifecycle

// Create provider
provider, err := hoistof.NewProvider(config)

// Register with OpenFeature
of.SetProvider(provider)

// ... use flags ...

// Cleanup on shutdown
provider.Close()

Error Handling

value, err := client.BooleanValue(ctx, "feature", false, evalCtx)
if err != nil {
    // Handle specific error types
    var resErr of.ResolutionError
    if errors.As(err, &resErr) {
        log.Printf("Resolution error: %s", resErr.Error())
    }
}

Provider Metadata

provider, _ := hoistof.NewProvider(config)
metadata := provider.Metadata()
fmt.Println(metadata.Name)  // "Hoist"

Best Practices

1. Single Provider Instance

// Good: Create once, reuse
var provider *hoistof.HoistProvider

func init() {
    provider, _ = hoistof.NewProvider(config)
    of.SetProvider(provider)
}

2. Use Evaluation Context

// Good: Always provide context
client.BooleanValue(ctx, "feature", false, of.EvaluationContext{
    TargetingKey: userID,
})

// Avoid: Empty context
client.BooleanValue(ctx, "feature", false, of.EvaluationContext{})

3. Handle Shutdown

func main() {
    provider, _ := hoistof.NewProvider(config)
    of.SetProvider(provider)
    defer provider.Close()

    // ... application code ...
}

Migration from Native SDK

Replace native Hoist calls with OpenFeature:

// Before (native SDK)
import "git.macco.dev/macco/hoist/sdks/go/hoist"

client, _ := hoist.NewClient(config)
enabled, _ := client.Bool(ctx, "feature", false, hoist.Context{
    TargetingKey: "user-123",
})

// After (OpenFeature)
import (
    hoistof "git.macco.dev/macco/hoist/sdks/go/openfeature"
    of "github.com/open-feature/go-sdk/openfeature"
)

provider, _ := hoistof.NewProvider(config)
of.SetProvider(provider)
client := of.NewClient("my-app")
enabled, _ := client.BooleanValue(ctx, "feature", false, of.EvaluationContext{
    TargetingKey: "user-123",
})

Next Steps