Skip to content

Commit

Permalink
Rename client.Client to client.Info (#4416)
Browse files Browse the repository at this point in the history
Includes documentation and how it's expected to be used by
both providers and consumers.

Fixes #4058

Signed-off-by: Juraci Paixão Kröhling <[email protected]>
  • Loading branch information
jpkrohling authored Nov 17, 2021
1 parent db4aa87 commit 7dbf1b8
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 36 deletions.
91 changes: 67 additions & 24 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package client contains generic representations of clients connecting to different receivers
// Package client contains generic representations of clients connecting to
// different receivers. Components, such as processors or exporters, can make
// use of this information to make decisions related to grouping of batches,
// tenancy, load balancing, tagging, among others.
//
// The structs defined here are typically used within the context that is
// propagated down the pipeline, with the values being produced by receivers,
// and consumed by processors and exporters.
//
// Producers
//
// Receivers are responsible for obtaining a client.Info from the current
// context and enhancing the client.Info with the net.Addr from the peer,
// storing a new client.Info into the context that it passes down. For HTTP
// requests, the net.Addr is typically the IP address of the client.
//
// Typically, however, receivers would delegate this processing to helpers such
// as the confighttp or configgrpc packages: both contain interceptors that will
// enhance the context with the client.Info, such that no actions are needed by
// receivers that are built using confighttp.HTTPServerSettings or
// configgrpc.GRPCServerSettings.
//
// Consumers
//
// Provided that the pipeline does not contain processors that would discard or
// rewrite the context, such as the batch processor, processors and exporters
// have access to the client.Info via client.FromContext. Among other usages,
// this data can be used to:
//
// - annotate data points with authentication data (username, tenant, ...)
//
// - route data points based on authentication data
//
// - rate limit client calls based on IP addresses
//
// Processors and exporters relying on the existence of data from the
// client.Info, should clearly document this as part of the component's README
// file.
package client // import "go.opentelemetry.io/collector/client"

import (
Expand All @@ -25,50 +62,56 @@ import (

type ctxKey struct{}

// Client represents a generic client that sends data to any receiver supported by the OT receiver
type Client struct {
IP string
// Info contains data related to the clients connecting to receivers.
type Info struct {
// Addr for the client connecting to this collector. Available in a
// best-effort basis, and generally reliable for receivers making use of
// confighttp.ToServer and configgrpc.ToServerOption.
Addr net.Addr
}

// NewContext takes an existing context and derives a new context with the client value stored on it
func NewContext(ctx context.Context, c *Client) context.Context {
// NewContext takes an existing context and derives a new context with the
// client.Info value stored on it.
func NewContext(ctx context.Context, c Info) context.Context {
return context.WithValue(ctx, ctxKey{}, c)
}

// FromContext takes a context and returns a Client value from it, if present.
func FromContext(ctx context.Context) (*Client, bool) {
c, ok := ctx.Value(ctxKey{}).(*Client)
return c, ok
// FromContext takes a context and returns a ClientInfo from it.
// When a ClientInfo isn't present, a new empty one is returned.
func FromContext(ctx context.Context) Info {
c, ok := ctx.Value(ctxKey{}).(Info)
if !ok {
c = Info{}
}
return c
}

// FromGRPC takes a GRPC context and tries to extract client information from it
func FromGRPC(ctx context.Context) (*Client, bool) {
func FromGRPC(ctx context.Context) (Info, bool) {
if p, ok := peer.FromContext(ctx); ok {
ip := parseIP(p.Addr.String())
if ip != "" {
return &Client{ip}, true
if ip != nil {
return Info{ip}, true
}
}
return nil, false
return Info{}, false
}

// FromHTTP takes a net/http Request object and tries to extract client information from it
func FromHTTP(r *http.Request) (*Client, bool) {
func FromHTTP(r *http.Request) (Info, bool) {
ip := parseIP(r.RemoteAddr)
if ip == "" {
return nil, false
if ip == nil {
return Info{}, false
}
return &Client{ip}, true
return Info{ip}, true
}

func parseIP(source string) string {
func parseIP(source string) net.Addr {
ipstr, _, err := net.SplitHostPort(source)
if err == nil {
return ipstr
source = ipstr
}
ip := net.ParseIP(source)
if ip != nil {
return ip.String()
return &net.IPAddr{
IP: net.ParseIP(source),
}
return ""
}
75 changes: 63 additions & 12 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package client contains generic representations of clients connecting to different receivers
// Package client contains generic representations of clients connecting to
// different receivers
package client

import (
Expand All @@ -25,16 +26,66 @@ import (
"google.golang.org/grpc/peer"
)

func TestClientContext(t *testing.T) {
ips := []string{
"1.1.1.1", "127.0.0.1", "1111", "ip",
func TestNewContext(t *testing.T) {
testCases := []struct {
desc string
cl Info
}{
{
desc: "valid client",
cl: Info{
Addr: &net.IPAddr{
IP: net.IPv4(1, 2, 3, 4),
},
},
},
{
desc: "nil client",
cl: Info{},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
ctx := NewContext(context.Background(), tC.cl)
assert.Equal(t, ctx.Value(ctxKey{}), tC.cl)
})
}
}

func TestFromContext(t *testing.T) {
testCases := []struct {
desc string
input context.Context
expected Info
}{
{
desc: "context with client",
input: context.WithValue(context.Background(), ctxKey{}, Info{
Addr: &net.IPAddr{
IP: net.IPv4(1, 2, 3, 4),
},
}),
expected: Info{
Addr: &net.IPAddr{
IP: net.IPv4(1, 2, 3, 4),
},
},
},
{
desc: "context without client",
input: context.Background(),
expected: Info{},
},
{
desc: "context with something else in the key",
input: context.WithValue(context.Background(), ctxKey{}, "unexpected!"),
expected: Info{},
},
}
for _, ip := range ips {
ctx := NewContext(context.Background(), &Client{ip})
c, ok := FromContext(ctx)
assert.True(t, ok)
assert.NotNil(t, c)
assert.Equal(t, c.IP, ip)
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
assert.Equal(t, FromContext(tC.input), tC.expected)
})
}
}

Expand All @@ -49,12 +100,12 @@ func TestParsingGRPC(t *testing.T) {
client, ok := FromGRPC(grpcCtx)
assert.True(t, ok)
assert.NotNil(t, client)
assert.Equal(t, client.IP, "192.168.1.1")
assert.Equal(t, client.Addr.String(), "192.168.1.1")
}

func TestParsingHTTP(t *testing.T) {
client, ok := FromHTTP(&http.Request{RemoteAddr: "192.168.1.2"})
assert.True(t, ok)
assert.NotNil(t, client)
assert.Equal(t, client.IP, "192.168.1.2")
assert.Equal(t, client.Addr.String(), "192.168.1.2")
}
61 changes: 61 additions & 0 deletions client/doc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright The OpenTelemetry Authors
//
// Licensed 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.

package client_test

import (
"context"
"fmt"
"net"

"go.opentelemetry.io/collector/client"
"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/model/pdata"
)

func Example_receiver() {
// Your receiver get a next consumer when it's constructed
var next consumer.Traces

// You'll convert the incoming data into pipeline data
td := pdata.NewTraces()

// You probably have a context with client metadata from your listener or
// scraper
ctx := context.Background()

// Get the client from the context: if it doesn't exist, FromContext will
// create one
cl := client.FromContext(ctx)

// Extract the client information based on your original context and set it
// to Addr
cl.Addr = &net.IPAddr{ // nolint
IP: net.IPv4(1, 2, 3, 4),
}

// When you are done, propagate the context down the pipeline to the next
// consumer
next.ConsumeTraces(ctx, td) // nolint
}

func Example_processor() {
// Your processor or exporter will receive a context, from which you get the
// client information
ctx := context.Background()
cl := client.FromContext(ctx)

// And use the information from the client as you need
fmt.Println(cl.Addr)
}

0 comments on commit 7dbf1b8

Please sign in to comment.