Skip to content

Commit 15c72d2

Browse files
Add feature-gate to delete original time field from parsed container logs (#33946)
**Description:** <Describe what has changed.> <!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> This PR adds support for removing the original `time` attribute/field from the parsed log record as it was suggested at #33389. Users should use the `Timestamp` field which holds the parsed time. This patch introduces this change behind a feature flag called `filelog.container.removeOriginalTimeField` which is disabled by default following the [feature lifecycle](https://github.com/open-telemetry/opentelemetry-collector/tree/main/featuregate#feature-lifecycle). **Link to tracking Issue:** <Issue number if applicable> #33389 **Testing:** <Describe what testing was performed and which tests were added.> 1. Added unit-test 2. Run with `./bin/otelcontribcol_linux_amd64 --config container_config.yaml --feature-gates=filelog.container.removeOriginalTimeField ` and verify the output: ```console 2024-07-08T14:26:50.936+0300 info LogsExporter {"kind": "exporter", "data_type": "logs", "name": "debug", "resource logs": 1, "log records": 2} 2024-07-08T14:26:50.936+0300 info ResourceLog #0 Resource SchemaURL: Resource attributes: -> k8s.namespace.name: Str(some) -> k8s.pod.name: Str(kube-controller-kind-control-plane) -> k8s.container.restart_count: Str(1) -> k8s.pod.uid: Str(49cc7c1fd3702c40b2686ea7486091d6) -> k8s.container.name: Str(kube-controller) ScopeLogs #0 ScopeLogs SchemaURL: InstrumentationScope LogRecord #0 ObservedTimestamp: 2024-07-08 11:26:50.836517638 +0000 UTC Timestamp: 2029-03-30 08:31:20.545192187 +0000 UTC SeverityText: SeverityNumber: Unspecified(0) Body: Str(INFO: log line here) Attributes: -> logtag: Str(F) -> log.file.path: Str(/var/log/pods/some_kube-controller-kind-control-plane_49cc7c1fd3702c40b2686ea7486091d6/kube-controller/1.log) -> log.iostream: Str(stdout) Trace ID: Span ID: Flags: 0 ``` **Documentation:** <Describe the documentation added.> Added. --------- Signed-off-by: ChrsMark <[email protected]> Co-authored-by: Andrzej Stencel <[email protected]>
1 parent ed8e8bf commit 15c72d2

File tree

5 files changed

+160
-3
lines changed

5 files changed

+160
-3
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: receiver/filelog
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add filelog.container.removeOriginalTimeField feature-flag for removing original time field
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: [33946]
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]

pkg/stanza/docs/operators/container.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ Configuration:
253253
</tr>
254254
</table>
255255

256+
256257
#### Parse multiline logs and recombine into a single one
257258

258259
If you are using the Docker format (or log tag indicators are not working),
@@ -319,3 +320,10 @@ receivers:
319320
</td>
320321
</tr>
321322
</table>
323+
324+
### Removing original time field
325+
326+
In order to remove the original time field from the log records users can enable the
327+
`filelog.container.removeOriginalTimeField` feature gate.
328+
The feature gate `filelog.container.removeOriginalTimeField` will be deprecated and eventually removed
329+
in the future, following the [feature lifecycle](https://github.com/open-telemetry/opentelemetry-collector/tree/main/featuregate#feature-lifecycle).

pkg/stanza/operator/parser/container/config.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"sync"
99

1010
"go.opentelemetry.io/collector/component"
11+
"go.opentelemetry.io/collector/featuregate"
12+
"go.uber.org/zap"
1113

1214
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
1315
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/errors"
@@ -17,9 +19,17 @@ import (
1719
)
1820

1921
const (
20-
operatorType = "container"
21-
recombineSourceIdentifier = "log.file.path"
22-
recombineIsLastEntry = "attributes.logtag == 'F'"
22+
operatorType = "container"
23+
recombineSourceIdentifier = "log.file.path"
24+
recombineIsLastEntry = "attributes.logtag == 'F'"
25+
removeOriginalTimeFieldFeatureFlag = "filelog.container.removeOriginalTimeField"
26+
)
27+
28+
var removeOriginalTimeField = featuregate.GlobalRegistry().MustRegister(
29+
removeOriginalTimeFieldFeatureFlag,
30+
featuregate.StageAlpha,
31+
featuregate.WithRegisterDescription("When enabled, deletes the original `time` field from the Log Attributes. Time is parsed to Timestamp field, which should be used instead."),
32+
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/33389"),
2333
)
2434

2535
func init() {
@@ -77,6 +87,14 @@ func (c Config) Build(set component.TelemetrySettings) (operator.Operator, error
7787
}
7888
}
7989

90+
if !removeOriginalTimeField.IsEnabled() {
91+
// https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/33389
92+
set.Logger.Info("`time` log record attribute will be removed in a future release. Switch now using the feature gate.",
93+
zap.String("attribute", "time"),
94+
zap.String("feature gate", removeOriginalTimeFieldFeatureFlag),
95+
)
96+
}
97+
8098
p := &Parser{
8199
ParserOperator: parserOperator,
82100
recombineParser: recombineParser,

pkg/stanza/operator/parser/container/parser.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,5 +356,10 @@ func parseTime(e *entry.Entry, layout string) error {
356356
}
357357
// timeutils.ParseGotime calls timeutils.SetTimestampYear before returning the timeValue
358358
e.Timestamp = timeValue
359+
360+
if removeOriginalTimeField.IsEnabled() {
361+
e.Delete(entry.NewAttributeField(parseFrom))
362+
}
363+
359364
return nil
360365
}

pkg/stanza/operator/parser/container/parser_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/stretchr/testify/require"
1212
"go.opentelemetry.io/collector/component/componenttest"
13+
"go.opentelemetry.io/collector/featuregate"
1314

1415
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
1516
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator"
@@ -378,3 +379,101 @@ func TestRecombineProcess(t *testing.T) {
378379
})
379380
}
380381
}
382+
383+
func TestProcessWithTimeRemovalFlag(t *testing.T) {
384+
385+
require.NoError(t, featuregate.GlobalRegistry().Set(removeOriginalTimeField.ID(), true))
386+
t.Cleanup(func() {
387+
require.NoError(t, featuregate.GlobalRegistry().Set(removeOriginalTimeField.ID(), false))
388+
})
389+
390+
cases := []struct {
391+
name string
392+
op func() (operator.Operator, error)
393+
input *entry.Entry
394+
expect *entry.Entry
395+
}{
396+
{
397+
"docker",
398+
func() (operator.Operator, error) {
399+
cfg := NewConfigWithID("test_id")
400+
cfg.AddMetadataFromFilePath = false
401+
cfg.Format = "docker"
402+
set := componenttest.NewNopTelemetrySettings()
403+
return cfg.Build(set)
404+
},
405+
&entry.Entry{
406+
Body: `{"log":"INFO: log line here","stream":"stdout","time":"2029-03-30T08:31:20.545192187Z"}`,
407+
},
408+
&entry.Entry{
409+
Attributes: map[string]any{
410+
"log.iostream": "stdout",
411+
},
412+
Body: "INFO: log line here",
413+
Timestamp: time.Date(2029, time.March, 30, 8, 31, 20, 545192187, time.UTC),
414+
},
415+
},
416+
{
417+
"docker_with_auto_detection",
418+
func() (operator.Operator, error) {
419+
cfg := NewConfigWithID("test_id")
420+
cfg.AddMetadataFromFilePath = false
421+
set := componenttest.NewNopTelemetrySettings()
422+
return cfg.Build(set)
423+
},
424+
&entry.Entry{
425+
Body: `{"log":"INFO: log line here","stream":"stdout","time":"2029-03-30T08:31:20.545192187Z"}`,
426+
},
427+
&entry.Entry{
428+
Attributes: map[string]any{
429+
"log.iostream": "stdout",
430+
},
431+
Body: "INFO: log line here",
432+
Timestamp: time.Date(2029, time.March, 30, 8, 31, 20, 545192187, time.UTC),
433+
},
434+
},
435+
{
436+
"docker_with_auto_detection_and_metadata_from_file_path",
437+
func() (operator.Operator, error) {
438+
cfg := NewConfigWithID("test_id")
439+
cfg.AddMetadataFromFilePath = true
440+
set := componenttest.NewNopTelemetrySettings()
441+
return cfg.Build(set)
442+
},
443+
&entry.Entry{
444+
Body: `{"log":"INFO: log line here","stream":"stdout","time":"2029-03-30T08:31:20.545192187Z"}`,
445+
Attributes: map[string]any{
446+
"log.file.path": "/var/log/pods/some_kube-scheduler-kind-control-plane_49cc7c1fd3702c40b2686ea7486091d3/kube-scheduler44/1.log",
447+
},
448+
},
449+
&entry.Entry{
450+
Attributes: map[string]any{
451+
"log.iostream": "stdout",
452+
"log.file.path": "/var/log/pods/some_kube-scheduler-kind-control-plane_49cc7c1fd3702c40b2686ea7486091d3/kube-scheduler44/1.log",
453+
},
454+
Body: "INFO: log line here",
455+
Resource: map[string]any{
456+
"k8s.pod.name": "kube-scheduler-kind-control-plane",
457+
"k8s.pod.uid": "49cc7c1fd3702c40b2686ea7486091d3",
458+
"k8s.container.name": "kube-scheduler44",
459+
"k8s.container.restart_count": "1",
460+
"k8s.namespace.name": "some",
461+
},
462+
Timestamp: time.Date(2029, time.March, 30, 8, 31, 20, 545192187, time.UTC),
463+
},
464+
},
465+
}
466+
467+
for _, tc := range cases {
468+
t.Run(tc.name, func(t *testing.T) {
469+
op, err := tc.op()
470+
require.NoError(t, err, "did not expect operator function to return an error, this is a bug with the test case")
471+
472+
err = op.Process(context.Background(), tc.input)
473+
require.NoError(t, err)
474+
require.Equal(t, tc.expect, tc.input)
475+
// Stop the operator
476+
require.NoError(t, op.Stop())
477+
})
478+
}
479+
}

0 commit comments

Comments
 (0)