Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `WithDefaultAttributes` to `go.opentelemetry.io/otel/metric/x` to support setting default attributes on instruments. (#8135)
- Add `Settable` to `go.opentelemetry.io/otel/metric/x` to allow reusing attribute options. (#8178)
- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8194)
- Support `http/json` in `otlptracehttp` (#8273)

### Changed

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 52 additions & 20 deletions exporters/otlp/otlptrace/otlptracehttp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ import (
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/counter"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/observ"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/otlpconfig"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/otlpjson"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/retry"
)

const contentTypeProto = "application/x-protobuf"
const (
contentTypeProto = "application/x-protobuf"
contentTypeJSON = "application/json"
)

// maxResponseBodySize is the maximum number of bytes to read from a response
// body. It is set to 4 MiB per the OTLP specification recommendation to
Expand Down Expand Up @@ -160,19 +164,11 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
pbRequest := &coltracepb.ExportTraceServiceRequest{
ResourceSpans: protoSpans,
}
rawRequest, err := proto.Marshal(pbRequest)
if err != nil {
return err
}

ctx, cancel := d.contextWithStop(ctx)
defer cancel()

if maxSize := d.cfg.MaxRequestSize; maxSize > 0 && len(rawRequest) > maxSize {
return fmt.Errorf("request body too large: exceeded %d bytes", maxSize)
}

request, err := d.newRequest(rawRequest)
request, err := d.newRequest(pbRequest)
if err != nil {
return err
}
Expand Down Expand Up @@ -232,19 +228,26 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
return nil
}

if resp.Header.Get("Content-Type") == "application/x-protobuf" {
var respProto coltracepb.ExportTraceServiceResponse
var respProto coltracepb.ExportTraceServiceResponse
switch resp.Header.Get("Content-Type") {
case contentTypeProto:
if err := proto.Unmarshal(respData.Bytes(), &respProto); err != nil {
return err
}
case contentTypeJSON:
if err := otlpjson.UnmarshalExportTraceServiceResponse(respData.Bytes(), &respProto); err != nil {
return err
}
default:
return nil
}

if respProto.PartialSuccess != nil {
msg := respProto.PartialSuccess.GetErrorMessage()
n := respProto.PartialSuccess.GetRejectedSpans()
if n != 0 || msg != "" {
err := internal.TracePartialSuccessError(n, msg)
uploadErr = errors.Join(uploadErr, err)
}
if respProto.PartialSuccess != nil {
msg := respProto.PartialSuccess.GetErrorMessage()
n := respProto.PartialSuccess.GetRejectedSpans()
if n != 0 || msg != "" {
err := internal.TracePartialSuccessError(n, msg)
uploadErr = errors.Join(uploadErr, err)
}
}
return nil
Expand Down Expand Up @@ -283,7 +286,24 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
}))
}

func (d *client) newRequest(body []byte) (request, error) {
func (d *client) marshalRequest(pbRequst *coltracepb.ExportTraceServiceRequest) ([]byte, error) {
if d.cfg.Protocol == otlpconfig.ProtocolHTTPJSON {
rawRequest, err := otlpjson.MarshalExportTraceServiceRequest(pbRequst)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body in json: %w", err)
}
return rawRequest, nil
}

rawRequest, err := proto.Marshal(pbRequst)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body in protobuf: %w", err)
}

return rawRequest, nil
}

func (d *client) newRequest(pbRequst *coltracepb.ExportTraceServiceRequest) (request, error) {
u := url.URL{Scheme: d.getScheme(), Host: d.cfg.Endpoint, Path: d.cfg.URLPath}
r, err := http.NewRequestWithContext(context.Background(), http.MethodPost, u.String(), http.NoBody)
if err != nil {
Expand All @@ -297,6 +317,18 @@ func (d *client) newRequest(body []byte) (request, error) {
r.Header.Set(k, v)
}
r.Header.Set("Content-Type", contentTypeProto)
if d.cfg.Protocol == otlpconfig.ProtocolHTTPJSON {
r.Header.Set("Content-Type", contentTypeJSON)
}

body, err := d.marshalRequest(pbRequst)
if err != nil {
return request{Request: r}, err
}

if maxSize := d.cfg.MaxRequestSize; maxSize > 0 && len(body) > maxSize {
return request{Request: r}, fmt.Errorf("request body too large: exceeded %d bytes", maxSize)
}

req := request{Request: r}
switch Compression(d.cfg.Compression) {
Expand Down
34 changes: 34 additions & 0 deletions exporters/otlp/otlptrace/otlptracehttp/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,40 @@ func TestEndToEnd(t *testing.T) {
ExpectedHeaders: customProxyHeader,
},
},
{
name: "with protobuf request encoding",
opts: []otlptracehttp.Option{
otlptracehttp.WithEncoding(otlptracehttp.EncodingProtobuf),
},
},
{
name: "with JSON request encoding",
opts: []otlptracehttp.Option{
otlptracehttp.WithEncoding(otlptracehttp.EncodingJSON),
},
},
{
name: "with JSON collector response encoding",
Comment thread
MrAlias marked this conversation as resolved.
opts: []otlptracehttp.Option{
otlptracehttp.WithEncoding(otlptracehttp.EncodingJSON),
},
mcCfg: mockCollectorConfig{
InjectContentType: "application/json",
},
},
{
name: "with JSON collector response encoding and partial success",
opts: []otlptracehttp.Option{
otlptracehttp.WithEncoding(otlptracehttp.EncodingJSON),
},
mcCfg: mockCollectorConfig{
InjectContentType: "application/json",
Partial: &coltracepb.ExportTracePartialSuccess{
RejectedSpans: 1,
ErrorMessage: "missing required attribute aaa",
},
},
},
}

for _, tc := range tests {
Expand Down
6 changes: 6 additions & 0 deletions exporters/otlp/otlptrace/otlptracehttp/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ the filepath to the client's private key to use in mTLS communication in PEM for
OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY takes precedence over OTEL_EXPORTER_OTLP_CLIENT_KEY.
The configuration can be overridden by [WithTLSClientConfig] option.

OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL (default: "http/protobuf") -
Comment thread
MrAlias marked this conversation as resolved.
the transport protocol the exporter uses. For OTLP/HTTP exporters, it indicates the encoding format of the payloads sent to the collector.
Supported value: "http/protobuf", "http/json".
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL takes precedence over OTEL_EXPORTER_OTLP_PROTOCOL.
The configuration can be overridden by [WithEncoding] option.

[W3C Baggage HTTP Header Content Format]: https://www.w3.org/TR/baggage/#header-content
*/
package otlptracehttp // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
Loading
Loading