diff --git a/docs/logs/extending-the-sdk/MyClassWithRedactionEnumerator.cs b/docs/logs/extending-the-sdk/MyClassWithRedactionEnumerator.cs new file mode 100644 index 00000000000..4e5f3107460 --- /dev/null +++ b/docs/logs/extending-the-sdk/MyClassWithRedactionEnumerator.cs @@ -0,0 +1,53 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections; +using System.Collections.Generic; + +internal class MyClassWithRedactionEnumerator : IReadOnlyList> +{ + private readonly IReadOnlyList> state; + + public MyClassWithRedactionEnumerator(IReadOnlyList> state) + { + this.state = state; + } + + public int Count => this.state.Count; + + public KeyValuePair this[int index] => this.state[index]; + + public IEnumerator> GetEnumerator() + { + foreach (var entry in this.state) + { + var entryVal = entry.Value; + if (entryVal != null && entryVal.ToString() != null && entryVal.ToString().Contains("")) + { + yield return new KeyValuePair(entry.Key, "newRedactedValueHere"); + } + else + { + yield return entry; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } +} diff --git a/docs/logs/extending-the-sdk/MyRedactionProcessor.cs b/docs/logs/extending-the-sdk/MyRedactionProcessor.cs new file mode 100644 index 00000000000..2ad8371ae69 --- /dev/null +++ b/docs/logs/extending-the-sdk/MyRedactionProcessor.cs @@ -0,0 +1,30 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using OpenTelemetry; +using OpenTelemetry.Logs; + +internal class MyRedactionProcessor : BaseProcessor +{ + public override void OnEnd(LogRecord logRecord) + { + if (logRecord.State is IReadOnlyList> listOfKvp) + { + logRecord.State = new MyClassWithRedactionEnumerator(listOfKvp); + } + } +} diff --git a/docs/logs/extending-the-sdk/Program.cs b/docs/logs/extending-the-sdk/Program.cs index 02bdf14bc4c..2ea9dad4953 100644 --- a/docs/logs/extending-the-sdk/Program.cs +++ b/docs/logs/extending-the-sdk/Program.cs @@ -28,7 +28,8 @@ public static void Main() builder.AddOpenTelemetry(options => { options.IncludeScopes = true; - options.AddProcessor(new MyProcessor("ProcessorA")) + options.AddProcessor(new MyRedactionProcessor()) + .AddProcessor(new MyProcessor("ProcessorA")) .AddProcessor(new MyProcessor("ProcessorB")) .AddProcessor(new SimpleLogRecordExportProcessor(new MyExporter("ExporterX"))) .AddMyExporter(); @@ -64,6 +65,9 @@ public static void Main() { logger.LogError("{name} is broken.", "refrigerator"); } + + // message will be redacted by MyRedactionProcessor + logger.LogInformation("OpenTelemetry {sensitiveString}.", ""); } internal struct Food diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt index e69de29bb2d..770b89a8996 100644 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +OpenTelemetry.Logs.LogRecord.FormattedMessage.set -> void +OpenTelemetry.Logs.LogRecord.State.set -> void +OpenTelemetry.Logs.LogRecord.StateValues.set -> void \ No newline at end of file diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..770b89a8996 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +OpenTelemetry.Logs.LogRecord.FormattedMessage.set -> void +OpenTelemetry.Logs.LogRecord.State.set -> void +OpenTelemetry.Logs.LogRecord.StateValues.set -> void \ No newline at end of file diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index b19ef2e6bfc..2b3bd30138f 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Exposed public setters for `LogRecord.State`, `LogRecord.StateValues`, + and `LogRecord.FormattedMessage`. + ([#3217](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3217)) + ## 1.3.0-beta.1 Released 2022-Apr-15 diff --git a/src/OpenTelemetry/Logs/LogRecord.cs b/src/OpenTelemetry/Logs/LogRecord.cs index a73d11c9cbd..1059ff09829 100644 --- a/src/OpenTelemetry/Logs/LogRecord.cs +++ b/src/OpenTelemetry/Logs/LogRecord.cs @@ -81,21 +81,21 @@ internal LogRecord( public EventId EventId { get; } - public string FormattedMessage { get; } + public string FormattedMessage { get; set; } /// - /// Gets the raw state attached to the log. Set to when is enabled. /// - public object State { get; } + public object State { get; set; } /// - /// Gets the parsed state values attached to the log. Set when is enabled /// otherwise . /// - public IReadOnlyList> StateValues { get; } + public IReadOnlyList> StateValues { get; set; } public Exception Exception { get; } diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs b/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs index 530edc97dc8..37dd5615106 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordTest.cs @@ -33,6 +33,13 @@ namespace OpenTelemetry.Logs.Tests { public sealed class LogRecordTest { + private enum Field + { + FormattedMessage, + State, + StateValues, + } + [Fact] public void CheckCategoryNameForLog() { @@ -243,6 +250,122 @@ public void CheckStateForExceptionLogged() Assert.Equal(message, state.ToString()); } + [Fact] + public void CheckStateCanBeSet() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: null); + var logger = loggerFactory.CreateLogger(); + + var message = $"This does not matter."; + logger.LogInformation(message); + + var logRecord = exportedItems[0]; + logRecord.State = "newState"; + + var expectedState = "newState"; + Assert.Equal(expectedState, logRecord.State); + } + + [Fact] + public void CheckStateValuesCanBeSet() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.ParseStateValues = true); + var logger = loggerFactory.CreateLogger(); + + logger.Log( + LogLevel.Information, + 0, + new List> { new KeyValuePair("Key1", "Value1") }, + null, + (s, e) => "OpenTelemetry!"); + + var logRecord = exportedItems[0]; + var expectedStateValues = new List> { new KeyValuePair("Key2", "Value2") }; + logRecord.StateValues = expectedStateValues; + + Assert.Equal(expectedStateValues, logRecord.StateValues); + } + + [Fact] + public void CheckFormattedMessageCanBeSet() + { + using var loggerFactory = InitializeLoggerFactory(out List exportedItems, configure: options => options.IncludeFormattedMessage = true); + var logger = loggerFactory.CreateLogger(); + + logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + var logRecord = exportedItems[0]; + var expectedFormattedMessage = "OpenTelemetry Good Night!"; + logRecord.FormattedMessage = expectedFormattedMessage; + + Assert.Equal(expectedFormattedMessage, logRecord.FormattedMessage); + } + + [Fact] + public void CheckStateCanBeSetByProcessor() + { + var exportedItems = new List(); + var exporter = new InMemoryExporter(exportedItems); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.AddProcessor(new RedactionProcessor(Field.State)); + options.AddInMemoryExporter(exportedItems); + }); + }); + + var logger = loggerFactory.CreateLogger(); + logger.LogInformation($"This does not matter."); + + var state = exportedItems[0].State as IReadOnlyList>; + Assert.Equal("newStateKey", state[0].Key.ToString()); + Assert.Equal("newStateValue", state[0].Value.ToString()); + } + + [Fact] + public void CheckStateValuesCanBeSetByProcessor() + { + var exportedItems = new List(); + var exporter = new InMemoryExporter(exportedItems); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.AddProcessor(new RedactionProcessor(Field.StateValues)); + options.AddInMemoryExporter(exportedItems); + options.ParseStateValues = true; + }); + }); + + var logger = loggerFactory.CreateLogger(); + logger.LogInformation("This does not matter."); + + var stateValue = exportedItems[0]; + Assert.Equal(new KeyValuePair("newStateValueKey", "newStateValueValue"), stateValue.StateValues[0]); + } + + [Fact] + public void CheckFormattedMessageCanBeSetByProcessor() + { + var exportedItems = new List(); + var exporter = new InMemoryExporter(exportedItems); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.AddProcessor(new RedactionProcessor(Field.FormattedMessage)); + options.AddInMemoryExporter(exportedItems); + options.IncludeFormattedMessage = true; + }); + }); + + var logger = loggerFactory.CreateLogger(); + logger.LogInformation("OpenTelemetry {Greeting} {Subject}!", "Hello", "World"); + + var item = exportedItems[0]; + Assert.Equal("OpenTelemetry Good Night!", item.FormattedMessage); + } + [Fact] public void CheckTraceIdForLogWithinDroppedActivity() { @@ -668,6 +791,32 @@ IEnumerator IEnumerable.GetEnumerator() } } + private class RedactionProcessor : BaseProcessor + { + private readonly Field fieldToUpdate; + + public RedactionProcessor(Field fieldToUpdate) + { + this.fieldToUpdate = fieldToUpdate; + } + + public override void OnEnd(LogRecord logRecord) + { + if (this.fieldToUpdate == Field.State) + { + logRecord.State = new List> { new KeyValuePair("newStateKey", "newStateValue") }; + } + else if (this.fieldToUpdate == Field.StateValues) + { + logRecord.StateValues = new List> { new KeyValuePair("newStateValueKey", "newStateValueValue") }; + } + else + { + logRecord.FormattedMessage = "OpenTelemetry Good Night!"; + } + } + } + private class ListState : IEnumerable> { private readonly List> list;