Casi Comuni

Esplora i casi d’uso più comuni per gli interceptor Goa, inclusi logging, metriche, caching e rate limiting.

Questa sezione presenta alcuni dei casi d’uso più comuni per gli interceptor Goa, fornendo esempi pratici e linee guida per l’implementazione.

Logging

Il logging è uno dei casi d’uso più comuni per gli interceptor. Un interceptor di logging può registrare dettagli sulle richieste e le risposte:

func LoggingInterceptor(logger *log.Logger) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return func(ctx context.Context, req interface{}) (interface{}, error) {
            // Registra i dettagli della richiesta
            reqID := uuid.New()
            logger.Printf("[%s] richiesta: %v", reqID, req)
            
            start := time.Now()
            res, err := e(ctx, req)
            duration := time.Since(start)
            
            if err != nil {
                // Registra gli errori
                logger.Printf("[%s] errore dopo %v: %v", reqID, duration, err)
                return nil, err
            }
            
            // Registra la risposta
            logger.Printf("[%s] risposta dopo %v: %v", reqID, duration, res)
            return res, nil
        }
    }
}

Migliori Pratiche per il Logging

Metriche

Gli interceptor di metriche raccolgono dati sulle prestazioni e l’utilizzo del servizio:

type Metrics struct {
    requestCount   *prometheus.CounterVec
    requestLatency *prometheus.HistogramVec
    errorCount     *prometheus.CounterVec
}

func NewMetrics() *Metrics {
    return &Metrics{
        requestCount: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "requests_total",
                Help: "Numero totale di richieste",
            },
            []string{"method"},
        ),
        requestLatency: prometheus.NewHistogramVec(
            prometheus.HistogramOpts{
                Name: "request_duration_seconds",
                Help: "Latenza delle richieste in secondi",
            },
            []string{"method"},
        ),
        errorCount: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "errors_total",
                Help: "Numero totale di errori",
            },
            []string{"method", "type"},
        ),
    }
}

func MetricsInterceptor(metrics *Metrics) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return func(ctx context.Context, req interface{}) (interface{}, error) {
            method := goa.MethodName(ctx)
            
            // Incrementa il contatore delle richieste
            metrics.requestCount.WithLabelValues(method).Inc()
            
            start := time.Now()
            res, err := e(ctx, req)
            duration := time.Since(start)
            
            // Registra la latenza
            metrics.requestLatency.WithLabelValues(method).Observe(duration.Seconds())
            
            if err != nil {
                // Incrementa il contatore degli errori
                errorType := reflect.TypeOf(err).String()
                metrics.errorCount.WithLabelValues(method, errorType).Inc()
            }
            
            return res, err
        }
    }
}

Caching

L’interceptor di caching può memorizzare i risultati delle chiamate per migliorare le prestazioni:

type Cache interface {
    Get(key string) (interface{}, bool)
    Set(key string, value interface{}, ttl time.Duration)
}

func CachingInterceptor(cache Cache, ttl time.Duration) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return func(ctx context.Context, req interface{}) (interface{}, error) {
            // Genera la chiave di cache
            key := generateCacheKey(ctx, req)
            
            // Controlla la cache
            if value, found := cache.Get(key); found {
                return value, nil
            }
            
            // Esegui la chiamata
            res, err := e(ctx, req)
            if err != nil {
                return nil, err
            }
            
            // Memorizza il risultato in cache
            cache.Set(key, res, ttl)
            return res, nil
        }
    }
}

func generateCacheKey(ctx context.Context, req interface{}) string {
    // Implementa la logica per generare una chiave univoca
    return fmt.Sprintf("%v-%v", goa.MethodName(ctx), req)
}

Rate Limiting

L’interceptor di rate limiting può proteggere il servizio dal sovraccarico:

type RateLimiter struct {
    tokens     chan struct{}
    interval   time.Duration
    maxTokens  int
}

func NewRateLimiter(rate int, interval time.Duration) *RateLimiter {
    rl := &RateLimiter{
        tokens:    make(chan struct{}, rate),
        interval:  interval,
        maxTokens: rate,
    }
    
    // Riempie il bucket dei token
    for i := 0; i < rate; i++ {
        rl.tokens <- struct{}{}
    }
    
    go rl.refill()
    return rl
}

func (rl *RateLimiter) refill() {
    ticker := time.NewTicker(rl.interval)
    for range ticker.C {
        for i := len(rl.tokens); i < rl.maxTokens; i++ {
            rl.tokens <- struct{}{}
        }
    }
}

func RateLimitInterceptor(rl *RateLimiter) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return func(ctx context.Context, req interface{}) (interface{}, error) {
            select {
            case <-rl.tokens:
                // Procedi con la richiesta
                return e(ctx, req)
            default:
                // Rate limit superato
                return nil, &goa.ServiceError{
                    Name:    "rate_limit_exceeded",
                    Message: "Troppe richieste, riprova più tardi",
                }
            }
        }
    }
}

Autenticazione

L’interceptor di autenticazione può verificare le credenziali e gestire l’autorizzazione:

type Authenticator interface {
    Authenticate(ctx context.Context, token string) (User, error)
}

func AuthInterceptor(auth Authenticator) func(goa.Endpoint) goa.Endpoint {
    return func(e goa.Endpoint) goa.Endpoint {
        return func(ctx context.Context, req interface{}) (interface{}, error) {
            // Estrai il token
            token := extractToken(ctx)
            if token == "" {
                return nil, &goa.ServiceError{
                    Name:    "unauthorized",
                    Message: "Token di autenticazione mancante",
                }
            }
            
            // Verifica il token
            user, err := auth.Authenticate(ctx, token)
            if err != nil {
                return nil, &goa.ServiceError{
                    Name:    "unauthorized",
                    Message: "Token di autenticazione non valido",
                }
            }
            
            // Aggiungi l'utente al contesto
            ctx = context.WithValue(ctx, "user", user)
            return e(ctx, req)
        }
    }
}

func extractToken(ctx context.Context) string {
    // Implementa la logica per estrarre il token dal contesto
    return ""
}

Combinazione degli Interceptor

Esempio di come combinare diversi interceptor in un’applicazione:

func main() {
    // Inizializza i componenti
    logger := log.New(os.Stdout, "", log.LstdFlags)
    metrics := NewMetrics()
    cache := NewCache()
    rateLimiter := NewRateLimiter(100, time.Second)
    auth := NewAuthenticator()

    // Crea gli interceptor
    logging := LoggingInterceptor(logger)
    metricsInt := MetricsInterceptor(metrics)
    caching := CachingInterceptor(cache, 5*time.Minute)
    rateLimit := RateLimitInterceptor(rateLimiter)
    authentication := AuthInterceptor(auth)

    // Crea gli endpoint
    endpoints := service.NewEndpoints(svc)

    // Applica gli interceptor nell'ordine corretto
    endpoints.Use(logging)         // Prima il logging
    endpoints.Use(metricsInt)      // Poi le metriche
    endpoints.Use(rateLimit)       // Poi il rate limiting
    endpoints.Use(authentication)  // Poi l'autenticazione
    endpoints.Use(caching)         // Infine il caching
}

Conclusione

Gli interceptor sono strumenti potenti per implementare funzionalità trasversali. Quando li utilizzi:

  • Considera l’ordine di applicazione
  • Mantieni la semplicità
  • Monitora le prestazioni
  • Testa accuratamente
  • Documenta il comportamento