Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/hookdeck/outpost/internal/config"
"github.com/hookdeck/outpost/internal/idgen"
"github.com/hookdeck/outpost/internal/infra"
"github.com/hookdeck/outpost/internal/logging"
"github.com/hookdeck/outpost/internal/migrator"
Expand Down Expand Up @@ -56,6 +57,16 @@ func run(mainContext context.Context, cfg *config.Config) error {
}
logger.Info("starting outpost", logFields...)

// Initialize ID generators
logger.Debug("configuring ID generators",
zap.String("event_template", cfg.IDTemplate.Event))
if err := idgen.Configure(idgen.IDTemplateConfig{
Event: cfg.IDTemplate.Event,
}); err != nil {
logger.Error("failed to configure ID generators", zap.Error(err))
return err
}

if err := runMigration(mainContext, cfg, logger); err != nil {
return err
}
Expand Down
7 changes: 7 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ type Config struct {

// Alert
Alert AlertConfig `yaml:"alert"`

// ID Template
IDTemplate IDTemplateConfig `yaml:"id_template"`
}

var (
Expand Down Expand Up @@ -184,6 +187,10 @@ func (c *Config) InitDefaults() {
HookdeckSourceURL: "https://hkdk.events/yhk665ljz3rn6l",
SentryDSN: "https://examplePublicKey@o0.ingest.sentry.io/0",
}

c.IDTemplate = IDTemplateConfig{
Event: "{{uuidv4}}",
}
}

func (c *Config) parseConfigFile(flagPath string, osInterface OSInterface) error {
Expand Down
6 changes: 6 additions & 0 deletions internal/config/id_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package config

// IDTemplateConfig is the configuration for ID generation templates
type IDTemplateConfig struct {
Event string `yaml:"event" env:"ID_TEMPLATE_EVENT" desc:"Go template for generating event IDs. Available functions: uuidv4, uuidv7, nanoid. Default: '{{uuidv4}}'" required:"N"`
}
38 changes: 38 additions & 0 deletions internal/idgen/benchmark_comparison_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package idgen

import (
"testing"

"github.com/google/uuid"
)

// Benchmark direct UUID generation (original approach)
func BenchmarkDirectUUIDv4(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = uuid.New().String()
}
}

func BenchmarkDirectUUIDv7(b *testing.B) {
for i := 0; i < b.N; i++ {
id, _ := uuid.NewV7()
_ = id.String()
}
}

// Benchmark template-based generation (new approach)
func BenchmarkTemplateUUIDv4(b *testing.B) {
gen, _ := NewIDGenerator("{{uuidv4}}")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = gen.Generate()
}
}

func BenchmarkTemplateUUIDv7(b *testing.B) {
gen, _ := NewIDGenerator("{{uuidv7}}")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = gen.Generate()
}
}
126 changes: 126 additions & 0 deletions internal/idgen/idgen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package idgen

import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"text/template"

"github.com/google/uuid"
)

var (
eventGenerator *IDGenerator
)

func init() {
// Initialize with default UUID v4 generator
eventGenerator, _ = NewIDGenerator("{{uuidv4}}")
}

// IDGenerator generates IDs based on a template
type IDGenerator struct {
template *template.Template
}

// NewIDGenerator creates a new ID generator with the given template string
func NewIDGenerator(templateStr string) (*IDGenerator, error) {
if templateStr == "" {
templateStr = "{{uuidv4}}"
}

// Create template with custom functions
tmpl := template.New("id").Funcs(template.FuncMap{
"uuidv4": func() string {
return uuid.New().String()
},
"uuidv7": func() string {
id, err := uuid.NewV7()
if err != nil {
// Fallback to v4 if v7 generation fails
return uuid.New().String()
}
return id.String()
},
"nanoid": func() string {
return generateNanoid(21) // default size of 21
},
})

// Parse template
parsed, err := tmpl.Parse(templateStr)
if err != nil {
return nil, fmt.Errorf("failed to parse ID template: %w", err)
}

return &IDGenerator{template: parsed}, nil
}

// Generate generates a new ID using the template
func (g *IDGenerator) Generate() (string, error) {
var buf bytes.Buffer
if err := g.template.Execute(&buf, nil); err != nil {
return "", fmt.Errorf("failed to generate ID: %w", err)
}
return buf.String(), nil
}

// generateNanoid generates a nanoid-like ID
// This is a simplified implementation inspired by nanoid
// Uses URL-safe base64 alphabet
func generateNanoid(size int) string {
// URL-safe alphabet (A-Za-z0-9_-)
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

bytes := make([]byte, size)
if _, err := rand.Read(bytes); err != nil {
// Fallback to UUID if random generation fails
return uuid.New().String()
}

// Map random bytes to alphabet
result := make([]byte, size)
for i := 0; i < size; i++ {
result[i] = alphabet[int(bytes[i])%len(alphabet)]
}

return string(result)
}

// Helper to encode bytes to base64 URL-safe string
func encodeBase64URL(b []byte) string {
return base64.RawURLEncoding.EncodeToString(b)
}

// IDTemplateConfig contains ID generation templates for different entity types
type IDTemplateConfig struct {
Event string
}

// Configure configures all ID generators based on the provided config
// This should be called once at application startup before any concurrent usage
func Configure(cfg IDTemplateConfig) error {
// Configure event generator if template is provided
if cfg.Event != "" {
gen, err := NewIDGenerator(cfg.Event)
if err != nil {
return fmt.Errorf("failed to configure event ID generator: %w", err)
}
eventGenerator = gen
}

return nil
}

// Event generates an event ID using the configured generator.
// Defaults to UUID v4 if not configured via Configure().
func Event() string {
id, err := eventGenerator.Generate()
if err != nil {
// Fallback to UUID v4 on error
return uuid.New().String()
}

return id
}
Loading
Loading