Skip to content

Commit d7a41f8

Browse files
authored
[exporter/faroexporter] Add Faro exporter logic to wireframe (#39016)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Faro exporter package enables exporting OpenTelemetry data to Faro-compatible endpoints. This makes it work. <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes #35319 <!--Describe what testing was performed and which tests were added.--> #### Testing Added unit tests. <!--Please delete paragraphs that you did not use before submitting.-->
1 parent c10a49f commit d7a41f8

File tree

7 files changed

+504
-13
lines changed

7 files changed

+504
-13
lines changed

.chloggen/faro-exporter.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: faroexporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Completes the implementation of the Faro exporter.
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [35319]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: []

exporter/faroexporter/exporter.go

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,46 @@
44
package faroexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/faroexporter"
55

66
import (
7+
"bytes"
78
"context"
9+
"encoding/json"
810
"errors"
911
"fmt"
12+
"io"
13+
"net/http"
1014
"net/url"
1115
"runtime"
16+
"strconv"
17+
"sync"
18+
"time"
1219

20+
faro "github.com/grafana/faro/pkg/go"
1321
"go.opentelemetry.io/collector/component"
1422
"go.opentelemetry.io/collector/consumer"
23+
"go.opentelemetry.io/collector/consumer/consumererror"
1524
"go.opentelemetry.io/collector/exporter"
25+
"go.opentelemetry.io/collector/exporter/exporterhelper"
1626
"go.opentelemetry.io/collector/pdata/plog"
1727
"go.opentelemetry.io/collector/pdata/ptrace"
28+
"go.uber.org/multierr"
1829
"go.uber.org/zap"
30+
31+
farotranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/faro"
1932
)
2033

2134
type faroExporter struct {
22-
config *Config
23-
// client *http.Client
35+
config *Config
36+
client *http.Client
2437
logger *zap.Logger
2538
settings component.TelemetrySettings
2639
userAgent string
2740
}
2841

42+
const (
43+
headerRetryAfter = "Retry-After"
44+
jsonContentType = "application/json"
45+
)
46+
2947
func newExporter(cfg component.Config, set exporter.Settings) (*faroExporter, error) {
3048
oCfg := cfg.(*Config)
3149

@@ -47,16 +65,97 @@ func newExporter(cfg component.Config, set exporter.Settings) (*faroExporter, er
4765
}, nil
4866
}
4967

50-
func (fe *faroExporter) start(_ context.Context, _ component.Host) error {
68+
func (fe *faroExporter) start(ctx context.Context, host component.Host) error {
69+
client, err := fe.config.ClientConfig.ToClient(ctx, host, fe.settings)
70+
if err != nil {
71+
return err
72+
}
73+
fe.client = client
5174
return nil
5275
}
5376

54-
func (fe *faroExporter) ConsumeTraces(_ context.Context, _ ptrace.Traces) error {
55-
return nil
77+
func (fe *faroExporter) ConsumeTraces(ctx context.Context, td ptrace.Traces) error {
78+
fp, err := farotranslator.TranslateFromTraces(ctx, td)
79+
if err != nil {
80+
return fmt.Errorf("failed to translate traces to faro payloads: %w", err)
81+
}
82+
return fe.consume(ctx, fp)
5683
}
5784

58-
func (fe *faroExporter) ConsumeLogs(_ context.Context, _ plog.Logs) error {
59-
return nil
85+
func (fe *faroExporter) ConsumeLogs(ctx context.Context, ld plog.Logs) error {
86+
fp, err := farotranslator.TranslateFromLogs(ctx, ld)
87+
if err != nil {
88+
return fmt.Errorf("failed to translate logs to faro payloads: %w", err)
89+
}
90+
return fe.consume(ctx, fp)
91+
}
92+
93+
func (fe *faroExporter) export(ctx context.Context, fp *faro.Payload) error {
94+
fe.logger.Debug("Preparing to make HTTP request", zap.String("endpoint", fe.config.Endpoint))
95+
request, err := json.Marshal(fp)
96+
if err != nil {
97+
return consumererror.NewPermanent(err)
98+
}
99+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fe.config.Endpoint, bytes.NewReader(request))
100+
if err != nil {
101+
return consumererror.NewPermanent(err)
102+
}
103+
req.Header.Set("Content-Type", jsonContentType)
104+
req.Header.Set("User-Agent", fe.userAgent)
105+
106+
resp, err := fe.client.Do(req)
107+
if err != nil {
108+
return fmt.Errorf("failed to make an HTTP request: %w", err)
109+
}
110+
defer resp.Body.Close()
111+
112+
if resp.StatusCode == http.StatusAccepted {
113+
return nil
114+
}
115+
116+
var errString string
117+
var formattedErr error
118+
bodyBytes, err := io.ReadAll(resp.Body)
119+
bodyContent := "unknown response"
120+
if err == nil {
121+
bodyContent = string(bodyBytes)
122+
}
123+
124+
errString = fmt.Sprintf(
125+
"error exporting items, request to %s responded with HTTP Status Code %d, Message=%s",
126+
fe.config.Endpoint, resp.StatusCode, bodyContent)
127+
formattedErr = newStatusFromMsgAndHTTPCode(errString, resp.StatusCode).Err()
128+
129+
if isRetryableStatusCode(resp.StatusCode) {
130+
retryAfter := 0
131+
isThrottleError := resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable
132+
if val := resp.Header.Get(headerRetryAfter); isThrottleError && val != "" {
133+
if seconds, err := strconv.Atoi(val); err == nil {
134+
retryAfter = seconds
135+
}
136+
}
137+
return exporterhelper.NewThrottleRetry(formattedErr, time.Duration(retryAfter)*time.Second)
138+
}
139+
140+
return consumererror.NewPermanent(formattedErr)
141+
}
142+
143+
func (fe *faroExporter) consume(ctx context.Context, fp []faro.Payload) error {
144+
var errs error
145+
var wg sync.WaitGroup
146+
wg.Add(len(fp))
147+
var mu sync.Mutex
148+
for _, p := range fp {
149+
go func() {
150+
defer wg.Done()
151+
err := fe.export(ctx, &p)
152+
mu.Lock()
153+
errs = multierr.Append(errs, err)
154+
mu.Unlock()
155+
}()
156+
}
157+
wg.Wait()
158+
return errs
60159
}
61160

62161
func (fe *faroExporter) Capabilities() consumer.Capabilities {

0 commit comments

Comments
 (0)