Skip to content
Open
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
12 changes: 12 additions & 0 deletions _examples/interceptor/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
GO_TEST_CMD = $(if $(shell which richgo),richgo test,go test)
export ELASTICSEARCH_URL=http://elastic:elastic@localhost:9200

test: ## Run tests
go run ./cmd/auth_provider/main.go
go run ./cmd/context_auth/main.go
go run ./cmd/custom_auth/main.go
go run ./cmd/custom_observability/main.go

setup:

.PHONY: test setup
68 changes: 68 additions & 0 deletions _examples/interceptor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Example: Interceptors

This example demonstrates how to use Interceptors to modify HTTP requests before they are sent to Elasticsearch.

Interceptors wrap the HTTP round-trip and can inspect or modify requests and responses.
They are configured via the `elasticsearch.Config.Interceptors` field:

```go
es, _ := elasticsearch.NewClient(elasticsearch.Config{
Interceptors: []elastictransport.InterceptorFunc{
func(next elastictransport.RoundTripFunc) elastictransport.RoundTripFunc {
return func(req *http.Request) (*http.Response, error) {
// Modify request before sending
return next(req)
}
},
},
})
```

## Dynamic Auth Provider

The [`cmd/auth_provider/main.go`](cmd/auth_provider/main.go) example demonstrates how to dynamically inject authentication credentials into requests.

This pattern is useful for scenarios where credentials may change at runtime, such as token refresh or credential rotation.

```bash
go run cmd/auth_provider/main.go
```

## Context-Based Auth Override

The [`cmd/context_auth/main.go`](cmd/context_auth/main.go) example demonstrates how to override authentication credentials on a per-request basis using `context.Context`.

This pattern is useful for multi-tenant applications or impersonation scenarios where different requests need different credentials.

```bash
go run cmd/context_auth/main.go
```

## Custom Auth (Kerberos/SPNEGO)

The [`cmd/custom_auth/main.go`](cmd/custom_auth/main.go) example demonstrates how to implement Kerberos/SPNEGO authentication with challenge-response handling.

The interceptor handles 401 responses with `WWW-Authenticate: Negotiate` by obtaining a token and retrying the request.

> **Note:** This example uses a mock implementation. In production, you would use a Kerberos library like [gokrb5](https://github.com/jcmturner/gokrb5) to obtain service tickets.

```bash
go run cmd/custom_auth/main.go
```

## Custom Observability

The [`cmd/custom_observability/main.go`](cmd/custom_observability/main.go) example demonstrates how to add custom observability to Elasticsearch requests using OpenTelemetry.

It shows three interceptors for:

* **Logging**: Request/response details using `slog`
* **Metrics**: Request counter and duration histogram
* **Tracing**: Distributed tracing with spans

> **Note:** The client has built-in observability functionality. Prefer using the built-in options where possible.

```bash
go run cmd/custom_observability/main.go
```

120 changes: 120 additions & 0 deletions _examples/interceptor/cmd/auth_provider/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

// This example demonstrates how to use Interceptors to dynamically
// inject authentication credentials into requests.
//
// Interceptors allow you to modify requests before they are sent,
// making them ideal for scenarios where credentials may change at
// runtime (e.g., token refresh, credential rotation).
package main

import (
"fmt"
"log/slog"
"net/http"
"sync"

"github.com/elastic/elastic-transport-go/v8/elastictransport"
"github.com/elastic/go-elasticsearch/v9"
"github.com/elastic/go-elasticsearch/v9/_examples/interceptor/internal/fake"
"github.com/elastic/go-elasticsearch/v9/_examples/interceptor/internal/redact"
)

func main() {
// Start a fake Elasticsearch server that logs incoming auth credentials
srv := fake.NewServer(
fake.WithLogFn(func(r *http.Request) {
username, password, _ := redact.BasicAuth(r)
slog.Info("server received request",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("username", username),
slog.String("password", password),
)
}),
fake.WithStatusCode(http.StatusOK),
fake.WithResponseBody([]byte(`{"cluster_name":"example"}`)),
fake.WithHeaders(func(h http.Header) {
h.Set("X-Elastic-Product", "Elasticsearch")
h.Set("Content-Type", "application/json")
}),
)
defer srv.Close()

// Create a credential provider that can be updated at runtime
authProvider := NewCredentialProvider("user1", "password1")

// Create an Elasticsearch client with a custom auth interceptor
es, err := elasticsearch.NewClient(elasticsearch.Config{
Addresses: []string{srv.URL()},
Interceptors: []elastictransport.InterceptorFunc{
DynamicAuthInterceptor(authProvider),
},
})
if err != nil {
panic(err)
}

// First request uses initial credentials
fmt.Println(">>> Sending request with initial credentials (user1)")
_, _ = es.Info()

// Update credentials (simulating credential rotation)
fmt.Println("\n>>> Rotating credentials to (user2)")
authProvider.Update("user2", "password2")

// Second request automatically uses the new credentials
fmt.Println("\n>>> Sending request with rotated credentials (user2)")
_, _ = es.Info()
}

// DynamicAuthInterceptor creates an interceptor that injects BasicAuth
// credentials from a CredentialProvider into each request.
func DynamicAuthInterceptor(provider *CredentialProvider) elastictransport.InterceptorFunc {
return func(next elastictransport.RoundTripFunc) elastictransport.RoundTripFunc {
return func(req *http.Request) (*http.Response, error) {
username, password := provider.Get()
req.SetBasicAuth(username, password)
return next(req)
}
}
}

// CredentialProvider holds credentials that can be safely updated at runtime.
type CredentialProvider struct {
mu sync.RWMutex
username string
password string
}

func NewCredentialProvider(username, password string) *CredentialProvider {
return &CredentialProvider{username: username, password: password}
}

func (p *CredentialProvider) Update(username, password string) {
p.mu.Lock()
defer p.mu.Unlock()
p.username = username
p.password = password
}

func (p *CredentialProvider) Get() (username, password string) {
p.mu.RLock()
defer p.mu.RUnlock()
return p.username, p.password
}
116 changes: 116 additions & 0 deletions _examples/interceptor/cmd/context_auth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

// This example demonstrates how to use Interceptors to override
// authentication credentials on a per-request basis using context.Context.
//
// This pattern is useful when different requests need different credentials,
// such as multi-tenant applications or impersonation scenarios.
package main

import (
"context"
"fmt"
"log/slog"
"net/http"

"github.com/elastic/elastic-transport-go/v8/elastictransport"
"github.com/elastic/go-elasticsearch/v9"
"github.com/elastic/go-elasticsearch/v9/_examples/interceptor/internal/fake"
"github.com/elastic/go-elasticsearch/v9/_examples/interceptor/internal/redact"
)

func main() {
// Start a fake Elasticsearch server that logs incoming auth credentials
srv := fake.NewServer(
fake.WithLogFn(func(r *http.Request) {
username, password, _ := redact.BasicAuth(r)
slog.Info("server received request",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("username", username),
slog.String("password", password),
)
}),
fake.WithStatusCode(http.StatusOK),
fake.WithResponseBody([]byte(`{"cluster_name":"example"}`)),
fake.WithHeaders(func(h http.Header) {
h.Set("X-Elastic-Product", "Elasticsearch")
h.Set("Content-Type", "application/json")
}),
)
defer srv.Close()

// Create an Elasticsearch client with default credentials and context auth interceptor
es, err := elasticsearch.NewClient(elasticsearch.Config{
Addresses: []string{srv.URL()},
Username: "default_user",
Password: "default_password",
Interceptors: []elastictransport.InterceptorFunc{
ContextAuthInterceptor(),
},
})
if err != nil {
panic(err)
}

// Request without context override uses default credentials
fmt.Println(">>> Sending request with default credentials")
_, _ = es.Info()

// Request with context override uses the specified credentials
fmt.Println("\n>>> Sending request with context override (tenant_a)")
ctx := WithBasicAuth(context.Background(), "tenant_a", "tenant_a_secret")
_, _ = es.Info(es.Info.WithContext(ctx))

// Another request with different context credentials
fmt.Println("\n>>> Sending request with context override (tenant_b)")
ctx = WithBasicAuth(context.Background(), "tenant_b", "tenant_b_secret")
_, _ = es.Info(es.Info.WithContext(ctx))

// Request without context override still uses default credentials
fmt.Println("\n>>> Sending request with default credentials again")
_, _ = es.Info()
}

// basicAuthKey is the context key for storing basic auth credentials.
type basicAuthKey struct{}

type basicAuthValue struct {
username string
password string
}

// WithBasicAuth returns a context with basic auth credentials attached.
// Use this to override the default client credentials for a specific request.
func WithBasicAuth(ctx context.Context, username, password string) context.Context {
return context.WithValue(ctx, basicAuthKey{}, basicAuthValue{username, password})
}

// ContextAuthInterceptor creates an interceptor that overrides BasicAuth
// credentials if they are present in the request's context.
// If no credentials are in the context, the request proceeds unchanged.
func ContextAuthInterceptor() elastictransport.InterceptorFunc {
return func(next elastictransport.RoundTripFunc) elastictransport.RoundTripFunc {
return func(req *http.Request) (*http.Response, error) {
if auth, ok := req.Context().Value(basicAuthKey{}).(basicAuthValue); ok {
req.SetBasicAuth(auth.username, auth.password)
}
return next(req)
}
}
}
Loading