Skip to content

Commit 771b818

Browse files
authored
[exporter/awss3exporter] add s3_base_prefix parameter (#42660)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description Adds a new optional parameter `s3_base_prefix` to the `awss3exporter` which is used as the root path for all files uploaded. This new parameter is helpful when using the `resource_attrs_to_s3/s3_prefix` override ability but you still want a root path to be used for all file uploads. <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes #42661 <!--Describe what testing was performed and which tests were added.--> #### Testing Added unit tests and ran the otel collector locally using a config file that specifies an `awss3` exporter with the new `s3_base_prefix` parameter <!--Describe the documentation added.--> #### Documentation Updated the readme and added new configuration examples <!--Please delete paragraphs that you did not use before submitting.-->
1 parent c2d585d commit 771b818

File tree

9 files changed

+307
-5
lines changed

9 files changed

+307
-5
lines changed
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: exporter/awss3exporter
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: adds configuration field 's3_base_prefix' to be able to set a base path for all S3 file uploads
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: [42661]
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: [user]

exporter/awss3exporter/README.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ The following exporter configuration parameters are supported.
2424
|:--------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------|
2525
| `region` | AWS region. | "us-east-1" |
2626
| `s3_bucket` | S3 bucket | |
27-
| `s3_prefix` | prefix for the S3 key (root directory inside bucket). | |
27+
| `s3_base_prefix` | root prefix for the S3 key applied to all files. | |
28+
| `s3_prefix` | prefix for the S3 key that can be overridden dynamically by `resource_attrs_to_s3` parameter. | |
2829
| `s3_partition_format` | filepath formatting for the partition; See [strftime](https://www.man7.org/linux/man-pages/man3/strftime.3.html) for format specification. | "year=%Y/month=%m/day=%d/hour=%H/minute=%M" |
2930
| `s3_partition_timezone` | timezone used to format partition | Local |
3031
| `role_arn` | the Role ARN to be assumed | |
@@ -145,6 +146,27 @@ metric/YYYY/MM/DD/HH/mm
145146
Optionally along with `s3_partition_format` you can provide `s3_partition_timezone` as name from IANA Time Zone
146147
database to change default local timezone to custom, for example `UTC` or `Europe/London`.
147148

149+
## Base Path Configuration
150+
151+
The `s3_base_prefix` option allows you to specify a root path inside the bucket that is not overridden by `resource_attrs_to_s3`. If provided, `s3_prefix` will be appended to this base path.
152+
153+
```yaml
154+
exporters:
155+
awss3:
156+
s3uploader:
157+
region: 'eu-central-1'
158+
s3_bucket: 'databucket'
159+
s3_base_prefix: 'environment/prod'
160+
s3_prefix: 'metric'
161+
s3_partition_format: '%Y/%m/%d/%H/%M'
162+
```
163+
164+
In this case, logs and traces would be stored in the following path format.
165+
166+
```console
167+
environment/prod/metric/YYYY/MM/DD/HH/mm
168+
```
169+
148170
## Data routing based on resource attributes
149171
When `resource_attrs_to_s3/s3_bucket` or `resource_attrs_to_s3/s3_prefix` is configured, the S3 bucket and/or prefix are dynamically derived from specified resource attributes in your data.
150172
If the attribute values are unavailable, the bucket and prefix will fall back to the values defined in `s3uploader/s3_bucket` and `s3uploader/s3_prefix` respectively.
@@ -170,6 +192,42 @@ databucket/metric/YYYY/MM/DD/HH/mm
170192
...
171193
```
172194

195+
## Base Path with Resource Attributes
196+
197+
When using both `s3_base_prefix` and `resource_attrs_to_s3/s3_prefix`, the `s3_base_prefix` is always used while `s3_prefix` can be dynamically overridden by resource attributes.
198+
199+
```yaml
200+
exporters:
201+
awss3:
202+
s3uploader:
203+
region: 'eu-central-1'
204+
s3_bucket: 'databucket'
205+
s3_base_prefix: 'environment/prod'
206+
s3_prefix: 'default-metric'
207+
s3_partition_format: '%Y/%m/%d/%H/%M'
208+
resource_attrs_to_s3:
209+
s3_prefix: "com.awss3.prefix"
210+
```
211+
212+
In this configuration:
213+
- **Base Prefix**: `environment/prod` (always included)
214+
- **Prefix**: Dynamically set from resource attribute `com.awss3.prefix` if available, otherwise falls back to `default-metric`
215+
216+
**Path format examples:**
217+
218+
```console
219+
# When resource attribute com.awss3.prefix = "service-a/metrics"
220+
environment/prod/service-a/metrics/YYYY/MM/DD/HH/mm
221+
222+
# When resource attribute com.awss3.prefix = "service-b/logs"
223+
environment/prod/service-b/logs/YYYY/MM/DD/HH/mm
224+
225+
# When resource attribute is unavailable (fallback)
226+
environment/prod/default-metric/YYYY/MM/DD/HH/mm
227+
```
228+
229+
This allows you to maintain consistent organizational structure (via base path) while dynamically routing different data types or services to specific subdirectories.
230+
173231
## Retry
174232

175233
Standard is the default retryer implementation used by service clients. See the [retry](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/aws/retry) package documentation for details on what errors are considered as retryable by the standard retryer implementation.

exporter/awss3exporter/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ type S3UploaderConfig struct {
2525
Region string `mapstructure:"region"`
2626
// S3Bucket is the bucket name to be uploaded to.
2727
S3Bucket string `mapstructure:"s3_bucket"`
28-
// S3Prefix is the key (directory) prefix to written to inside the bucket
28+
// S3BasePrefix is the root key (directory) prefix used to write the file.
29+
S3BasePrefix string `mapstructure:"s3_base_prefix"`
30+
// S3Prefix is the key (directory) prefix to write to inside the bucket. Appended to S3BasePrefix if provided.
2931
S3Prefix string `mapstructure:"s3_prefix"`
3032
// S3PartitionFormat is used to provide the rollup on how data is written. Uses [strftime](https://www.man7.org/linux/man-pages/man3/strftime.3.html) formatting.
3133
S3PartitionFormat string `mapstructure:"s3_partition_format"`

exporter/awss3exporter/config_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,84 @@ func TestConfigS3UniqueKeyFunc(t *testing.T) {
557557
}, e,
558558
)
559559
}
560+
561+
func TestConfigS3BasePrefix(t *testing.T) {
562+
factories, err := otelcoltest.NopFactories()
563+
assert.NoError(t, err)
564+
565+
factory := NewFactory()
566+
factories.Exporters[factory.Type()] = factory
567+
cfg, err := otelcoltest.LoadConfigAndValidate(
568+
filepath.Join("testdata", "config-s3_base_prefix.yaml"), factories)
569+
570+
require.NoError(t, err)
571+
require.NotNil(t, cfg)
572+
573+
e := cfg.Exporters[component.MustNewID("awss3")].(*Config)
574+
queueCfg := exporterhelper.NewDefaultQueueConfig()
575+
queueCfg.Enabled = false
576+
timeoutCfg := exporterhelper.TimeoutConfig{
577+
Timeout: 5 * time.Second,
578+
}
579+
580+
assert.Equal(t, &Config{
581+
S3Uploader: S3UploaderConfig{
582+
Region: "us-east-1",
583+
S3Bucket: "foo",
584+
S3Prefix: "bar",
585+
S3BasePrefix: "base/prefix",
586+
S3PartitionFormat: "year=%Y/month=%m/day=%d/hour=%H/minute=%M",
587+
Endpoint: "http://endpoint.com",
588+
StorageClass: "STANDARD",
589+
RetryMode: DefaultRetryMode,
590+
RetryMaxAttempts: DefaultRetryMaxAttempts,
591+
RetryMaxBackoff: DefaultRetryMaxBackoff,
592+
},
593+
QueueSettings: queueCfg,
594+
TimeoutSettings: timeoutCfg,
595+
MarshalerName: "otlp_json",
596+
}, e,
597+
)
598+
}
599+
600+
func TestConfigS3BasePrefixWithResourceAttrs(t *testing.T) {
601+
factories, err := otelcoltest.NopFactories()
602+
assert.NoError(t, err)
603+
604+
factory := NewFactory()
605+
factories.Exporters[factory.Type()] = factory
606+
cfg, err := otelcoltest.LoadConfigAndValidate(
607+
filepath.Join("testdata", "config-s3_base_prefix_with_resource_attrs.yaml"), factories)
608+
609+
require.NoError(t, err)
610+
require.NotNil(t, cfg)
611+
612+
e := cfg.Exporters[component.MustNewID("awss3")].(*Config)
613+
queueCfg := exporterhelper.NewDefaultQueueConfig()
614+
queueCfg.Enabled = false
615+
timeoutCfg := exporterhelper.TimeoutConfig{
616+
Timeout: 5 * time.Second,
617+
}
618+
619+
assert.Equal(t, &Config{
620+
S3Uploader: S3UploaderConfig{
621+
Region: "us-east-1",
622+
S3Bucket: "foo",
623+
S3Prefix: "default-metric",
624+
S3BasePrefix: "environment/prod",
625+
S3PartitionFormat: "year=%Y/month=%m/day=%d/hour=%H/minute=%M",
626+
Endpoint: "http://endpoint.com",
627+
StorageClass: "STANDARD",
628+
RetryMode: DefaultRetryMode,
629+
RetryMaxAttempts: DefaultRetryMaxAttempts,
630+
RetryMaxBackoff: DefaultRetryMaxBackoff,
631+
},
632+
QueueSettings: queueCfg,
633+
TimeoutSettings: timeoutCfg,
634+
MarshalerName: "otlp_json",
635+
ResourceAttrsToS3: ResourceAttrsToS3{
636+
S3Prefix: "com.awss3.prefix",
637+
},
638+
}, e,
639+
)
640+
}

exporter/awss3exporter/internal/upload/partition.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math/rand/v2"
88
"path"
99
"strconv"
10+
"strings"
1011
"time"
1112

1213
"github.com/google/uuid"
@@ -19,8 +20,12 @@ var compressionFileExtensions = map[configcompression.Type]string{
1920
}
2021

2122
type PartitionKeyBuilder struct {
23+
// PartitionBasePrefix defines the root S3
24+
// directory (key) prefix used to write the file.
25+
PartitionBasePrefix string
2226
// PartitionPrefix defines the S3 directory (key)
23-
// prefix used to write the file
27+
// prefix used to write the file.
28+
// Appended to PartitionBasePrefix if provided.
2429
PartitionPrefix string
2530
// PartitionFormat is used to separate values into
2631
// different time buckets.
@@ -57,14 +62,24 @@ func (pki *PartitionKeyBuilder) bucketKeyPrefix(ts time.Time, overridePrefix str
5762
if overridePrefix != "" {
5863
prefix = overridePrefix
5964
}
65+
66+
var pathParts []string
67+
68+
if pki.PartitionBasePrefix != "" {
69+
pathParts = append(pathParts, pki.PartitionBasePrefix)
70+
}
71+
6072
if prefix != "" {
61-
prefix += "/"
73+
pathParts = append(pathParts, prefix)
6274
}
75+
6376
location := pki.PartitionTimeLocation
6477
if location == nil {
6578
location = time.Local
6679
}
67-
return prefix + timefmt.Format(ts.In(location), pki.PartitionFormat)
80+
pathParts = append(pathParts, timefmt.Format(ts.In(location), pki.PartitionFormat))
81+
82+
return strings.Join(pathParts, "/")
6883
}
6984

7085
func (pki *PartitionKeyBuilder) fileName() string {

exporter/awss3exporter/internal/upload/partition_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,69 @@ func TestPartitionKeyInputsNewPartitionKey(t *testing.T) {
102102
expect: "/foo-prefix1/year=2024/month=01/day=24/hour=06/minute=40/signal-output-service-01_pod2_fixed.metrics.gz",
103103
overridePrefix: "/foo-prefix1",
104104
},
105+
{
106+
name: "base path only",
107+
inputs: &PartitionKeyBuilder{
108+
PartitionBasePrefix: "base/path",
109+
PartitionFormat: "year=%Y/month=%m/day=%d/hour=%H/minute=%M",
110+
FilePrefix: "signal-output-",
111+
Metadata: "service-01_pod2",
112+
FileFormat: "metrics",
113+
UniqueKeyFunc: func() string {
114+
return "fixed"
115+
},
116+
},
117+
expect: "base/path/year=2024/month=01/day=24/hour=06/minute=40/signal-output-service-01_pod2_fixed.metrics",
118+
overridePrefix: "",
119+
},
120+
{
121+
name: "base path with prefix",
122+
inputs: &PartitionKeyBuilder{
123+
PartitionBasePrefix: "base/path",
124+
PartitionPrefix: "telemetry",
125+
PartitionFormat: "year=%Y/month=%m/day=%d/hour=%H/minute=%M",
126+
FilePrefix: "signal-output-",
127+
Metadata: "service-01_pod2",
128+
FileFormat: "metrics",
129+
UniqueKeyFunc: func() string {
130+
return "fixed"
131+
},
132+
},
133+
expect: "base/path/telemetry/year=2024/month=01/day=24/hour=06/minute=40/signal-output-service-01_pod2_fixed.metrics",
134+
overridePrefix: "",
135+
},
136+
{
137+
name: "base path with prefix and override",
138+
inputs: &PartitionKeyBuilder{
139+
PartitionBasePrefix: "base/path",
140+
PartitionPrefix: "telemetry",
141+
PartitionFormat: "year=%Y/month=%m/day=%d/hour=%H/minute=%M",
142+
FilePrefix: "signal-output-",
143+
Metadata: "service-01_pod2",
144+
FileFormat: "metrics",
145+
UniqueKeyFunc: func() string {
146+
return "fixed"
147+
},
148+
},
149+
expect: "base/path/override/year=2024/month=01/day=24/hour=06/minute=40/signal-output-service-01_pod2_fixed.metrics",
150+
overridePrefix: "override",
151+
},
152+
{
153+
name: "base path with empty prefix",
154+
inputs: &PartitionKeyBuilder{
155+
PartitionBasePrefix: "base/path",
156+
PartitionPrefix: "",
157+
PartitionFormat: "year=%Y/month=%m/day=%d/hour=%H/minute=%M",
158+
FilePrefix: "signal-output-",
159+
Metadata: "service-01_pod2",
160+
FileFormat: "metrics",
161+
UniqueKeyFunc: func() string {
162+
return "fixed"
163+
},
164+
},
165+
expect: "base/path/year=2024/month=01/day=24/hour=06/minute=40/signal-output-service-01_pod2_fixed.metrics",
166+
overridePrefix: "",
167+
},
105168
} {
106169
t.Run(tc.name, func(t *testing.T) {
107170
t.Parallel()

exporter/awss3exporter/s3_writer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ func newUploadManager(
101101
return upload.NewS3Manager(
102102
conf.S3Uploader.S3Bucket,
103103
&upload.PartitionKeyBuilder{
104+
PartitionBasePrefix: conf.S3Uploader.S3BasePrefix,
104105
PartitionPrefix: conf.S3Uploader.S3Prefix,
105106
PartitionFormat: conf.S3Uploader.S3PartitionFormat,
106107
PartitionTimeLocation: s3PartitionTimeLocation,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
receivers:
2+
nop:
3+
4+
exporters:
5+
awss3:
6+
sending_queue:
7+
enabled: false
8+
timeout: 5s
9+
10+
s3uploader:
11+
region: 'us-east-1'
12+
s3_bucket: 'foo'
13+
s3_base_prefix: 'base/prefix'
14+
s3_prefix: 'bar'
15+
s3_partition_format: 'year=%Y/month=%m/day=%d/hour=%H/minute=%M'
16+
endpoint: "http://endpoint.com"
17+
18+
processors:
19+
nop:
20+
21+
service:
22+
pipelines:
23+
traces:
24+
receivers: [nop]
25+
processors: [nop]
26+
exporters: [awss3]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
receivers:
2+
nop:
3+
4+
exporters:
5+
awss3:
6+
sending_queue:
7+
enabled: false
8+
timeout: 5s
9+
10+
s3uploader:
11+
region: 'us-east-1'
12+
s3_bucket: 'foo'
13+
s3_base_prefix: 'environment/prod'
14+
s3_prefix: 'default-metric'
15+
s3_partition_format: 'year=%Y/month=%m/day=%d/hour=%H/minute=%M'
16+
endpoint: "http://endpoint.com"
17+
18+
resource_attrs_to_s3:
19+
s3_prefix: "com.awss3.prefix"
20+
21+
processors:
22+
nop:
23+
24+
service:
25+
pipelines:
26+
traces:
27+
receivers: [nop]
28+
processors: [nop]
29+
exporters: [awss3]

0 commit comments

Comments
 (0)