Skip to content

Conversation

@minuk-dev
Copy link

@minuk-dev minuk-dev commented Nov 13, 2025

Summary

### Question
- I checked other plugins' directories, but is it a coding convention to keep everything within a single file? There's too much logic, so I want to split the file.
Solved: #17997 (comment)

Local Test

  • You can test this feature using below config.
otel.yaml
# otel.yaml
receivers:
  otlp:
    protocols:
        # OTLP/HTTP standard port is 4318
        endpoint: "http://0.0.0.0:4318"

exporters:
  debug:

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: []
      exporters: [debug]

    metrics:
      receivers: [otlp]
      processors: []
      exporters: [debug]

    logs:
      receivers: [otlp]
      processors: []
      exporters: [debug]
docker run --rm -it -v "$(pwd)/otel.yaml":/etc/otel/config.yaml -p 4318:4318 --name otel-collector otel/opentelemetry-collector:latest --config /etc/otel/config.yaml
# telegraf.conf
# skip
[[outputs.opentelemetry]]
protocol = "http"
service_address = "http://localhost:4318/v1/metrics"
image

Checklist

Related issues

@telegraf-tiger
Copy link
Contributor

Thanks so much for the pull request!
🤝 ✒️ Just a reminder that the CLA has not yet been signed, and we'll need it before merging. Please sign the CLA when you get a chance, then post a comment here saying !signed-cla

@telegraf-tiger telegraf-tiger bot added feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin plugin/output 1. Request for new output plugins 2. Issues/PRs that are related to out plugins labels Nov 13, 2025
@minuk-dev minuk-dev marked this pull request as ready for review November 13, 2025 19:42
Copy link
Member

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @minuk-dev for your contribution! I do have some initial comments below. Let's first get the code structure right and then work on the details...

@srebhan srebhan self-assigned this Nov 17, 2025
@srebhan srebhan changed the title feat(outputs.opentelemetry): support http protocol with protobuf & json encoding feat(outputs.opentelemetry): Support http protocol Nov 17, 2025
@minuk-dev minuk-dev marked this pull request as draft November 17, 2025 14:18
@minuk-dev minuk-dev marked this pull request as ready for review November 26, 2025 15:56
Copy link
Member

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @minuk-dev for the update! Here is round two of my comments... ;-)

Comment on lines 215 to 217
const (
maxHTTPResponseReadBytes = 64 * 1024 // 64 KB
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't declare this const but just use the value!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed it. But, why?

feaa9c4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is used only in one spot and you can interpret the value from the assignment there. Having this indirection makes reviews harder e.g. if someone changes the value here the consequences in the code are not clear. Use constants if they add value e.g. if you check a numeric return code it's better to write

const errnoUnauthorized = 5

...

if errno == errnoUnauthorized {...}

instead of

if errno == 5 {...}

or if the value might be changed later and is used in multiple places.

@srebhan srebhan added the waiting for response waiting for response from contributor label Dec 10, 2025
@minuk-dev minuk-dev requested a review from srebhan December 14, 2025 12:42
@srebhan srebhan removed the waiting for response waiting for response from contributor label Dec 18, 2025
Copy link
Member

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update @minuk-dev! Some more updates...

} else {
h.httpClient.Transport = &http.Transport{
TLSClientConfig: &ntls.Config{
InsecureSkipVerify: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm we can't enforce this insecure setting! If at all, this should be a user setting.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to find other output plugins how to handle it.
Please wait for a while.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. I fixed it. I made a mistake.

Comment on lines 91 to 97
defer func() {
//nolint:errcheck // cannot fail with io.Discard
// 64KB is a not specific limit. But, opentelemetry-collector also uses 64KB for safety.
io.CopyN(io.Discard, httpResponse.Body, 64*1024)
_ = httpResponse.Body.Close()
}()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this complexity? Wouldn't

Suggested change
defer func() {
//nolint:errcheck // cannot fail with io.Discard
// 64KB is a not specific limit. But, opentelemetry-collector also uses 64KB for safety.
io.CopyN(io.Discard, httpResponse.Body, 64*1024)
_ = httpResponse.Body.Close()
}()
if _, err := io.Copy(io.Discard, httpResponse.Body) {
return pmetricotlp.ExportResponse{}, err
}

be good enough?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the server sends very big response unexpectedly, two codes work differently.
So, I think we should use io.CopyN() instead of io.Copy().

Do you think this defensive code is not needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Citing the documentation for http.Response:

// The http Client and Transport guarantee that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.

So either you have to read the whole body if you care for reusing persistent TCP connections or you don't care and then you can just close the body.

Remember that the body is streamed from the connection so if the server sends a "very big response" it will be read and discarded i.e. you read the response from a chunk of memory or a network buffer in the worst case (also memory) and do nothing with it... Which negative effect to you expect from this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure your suggestion is really better. But, I'll change it.
Below is the detailed reason.


Sample code: https://go.dev/play/p/T3vZ1MH-eeK

I use io.CopyN here deliberately as a defensive choice.

This client does not consume the response body, so the goal is to avoid unbounded reads
rather than to maximize connection reuse.

With io.Copy, the client will read until EOF. If the server returns a very large body
or a chunked response that does not terminate properly (e.g. a misbehaving server or
proxy issue), the client would keep reading and consume CPU and network resources.

By limiting the read with io.CopyN, we cap how much data we are willing to consume and
stop early in those cases, even though it means the connection will not be reused.

In this context, protecting client resources is preferred over keep-alive reuse.

You can find many usecases in github.
https://github.com/search?q=io.CopyN%28io.Discard%2C+res.Body&type=code

Comment on lines 130 to 163
func readResponseBody(resp *http.Response) ([]byte, error) {
if resp.ContentLength == 0 {
return nil, nil
}

maxRead := resp.ContentLength

// if maxRead == -1, the ContentLength header has not been sent, so read up to
// the maximum permitted body size. If it is larger than the permitted body
// size, still try to read from the body in case the value is an error. If the
// body is larger than the maximum size, proto unmarshaling will likely fail.
// 64KB is a not specific limit. But, opentelemetry-collector also uses 64KB for safety.
if maxRead == -1 || maxRead > 64*1024 {
maxRead = 64 * 1024
}
protoBytes := make([]byte, maxRead)
n, err := io.ReadFull(resp.Body, protoBytes)

// No bytes read and an EOF error indicates there is no body to read.
if n == 0 && (err == nil || errors.Is(err, io.EOF)) {
return nil, nil
}

// io.ReadFull will return io.ErrorUnexpectedEOF if the Content-Length header
// wasn't set, since we will try to read past the length of the body. If this
// is the case, the body will still have the full message in it, so we want to
// ignore the error and parse the message.
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
return nil, err
}

return protoBytes[:n], nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think you should use io.LimitedReader instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they are different. But, I understand you and I'm not sure about it.
I check it.

Copy link
Member

@srebhan srebhan Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I can follow... The body is a ReadCloser so I would do (in the caller side of the code)

	// Make sure we close the body
	defer httpResponse.Body.Close()
	
	// Read the body up to the maximum length
	r := io.LimitReader(httpResponse.Body, maxBodyLength)
	body, err := io.ReadAll(r)
	if err != nil {
		return pmetricotlp.ExportResponse{}, fmt.Errorf("reading response body failed: '%w'", err)
	}

	// Discard the rest of the body
	n, err := io.Copy(io.Discard, httpResponse.Body)
	if err != nil {
		return pmetricotlp.ExportResponse{}, fmt.Errorf("discarding remaining response body failed: '%w'", err)
	}
	if n > 0 {
		return pmetricotlp.ExportResponse{}, fmt.Errorf("response body %d byte larger than supported maximum", n)
	}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to use resp.ContentLength in your version.
We don't have to discard the rest of the body.

So, I changed it like this: 17a6922.

Copy link
Member

@srebhan srebhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @minuk-dev! Please see my comment and also my responses above.

Comment on lines +108 to +120
exportResponse := pmetricotlp.NewExportResponse()
switch httpResponse.Header.Get("Content-Type") {
case "application/x-protobuf":
err = exportResponse.UnmarshalProto(responseBytes)
if err != nil {
return pmetricotlp.ExportResponse{}, err
}
case "application/json":
err = exportResponse.UnmarshalJSON(responseBytes)
if err != nil {
return pmetricotlp.ExportResponse{}, err
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this must be

Suggested change
exportResponse := pmetricotlp.NewExportResponse()
switch httpResponse.Header.Get("Content-Type") {
case "application/x-protobuf":
err = exportResponse.UnmarshalProto(responseBytes)
if err != nil {
return pmetricotlp.ExportResponse{}, err
}
case "application/json":
err = exportResponse.UnmarshalJSON(responseBytes)
if err != nil {
return pmetricotlp.ExportResponse{}, err
}
}
exportResponse := pmetricotlp.NewExportResponse()
switch httpResponse.Header.Get("Content-Type") {
case "protobuf":
if err := exportResponse.UnmarshalProto(responseBytes); err != nil {
return pmetricotlp.ExportResponse{}, err
}
case "json":
if err := exportResponse.UnmarshalJSON(responseBytes); err != nil {
return pmetricotlp.ExportResponse{}, err
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contentType is from the http response's header.
So, we cannot use it like that.

@telegraf-tiger
Copy link
Contributor

telegraf-tiger bot commented Jan 1, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Improvement on an existing feature such as adding a new setting/mode to an existing plugin plugin/output 1. Request for new output plugins 2. Issues/PRs that are related to out plugins

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants