diff --git a/src/Agent/CHANGELOG.md b/src/Agent/CHANGELOG.md index c923459cd0..e4953265c5 100644 --- a/src/Agent/CHANGELOG.md +++ b/src/Agent/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New Features ### Fixes +* Resolves an issue where log forwarding could drop logs in async scenarios. [#1174](https://github.com/newrelic/newrelic-dotnet-agent/pull/1201) ## [10.0.0] - 2022-07-19 diff --git a/src/Agent/NewRelic/Agent/Core/Agent.cs b/src/Agent/NewRelic/Agent/Core/Agent.cs index 0a44515466..23f6e05de1 100644 --- a/src/Agent/NewRelic/Agent/Core/Agent.cs +++ b/src/Agent/NewRelic/Agent/Core/Agent.cs @@ -406,7 +406,7 @@ public void RecordSupportabilityMetric(string metricName, int count) _agentHealthReporter.ReportSupportabilityCountMetric(metricName, count); } - public void RecordLogMessage(string frameworkName, object logEvent, Func getTimestamp, Func getLevel, Func getLogMessage, string spanId, string traceId) + public void RecordLogMessage(string frameworkName, object logEvent, Func getTimestamp, Func getLevel, Func getLogMessage, string spanId, string traceId) { _agentHealthReporter.ReportLogForwardingFramework(frameworkName); @@ -424,7 +424,7 @@ public void RecordLogMessage(string frameworkName, object logEvent, Func IList Segments { get; } - IList LogEvents { get; } ICandidateTransactionName CandidateTransactionName { get; } void RollupTransactionNameByStatusCodeIfNeeded(); ITransactionMetadata TransactionMetadata { get; } @@ -82,5 +81,19 @@ public interface IInternalTransaction : ITransaction, ITransactionExperimental ITransactionSegmentState GetTransactionSegmentState(); void NoticeError(ErrorData errorData); + + /// + /// Harvests the log events from the transaction. After doing this, no more logs can be added to the transaction. + /// + /// The accumulated logs on the first call, or null if logs have already been harvested + IList HarvestLogEvents(); + + /// + /// Attempts to add a log to the current transaction. Logs cannot be added after the transaction transform has + /// harvested logs. + /// + /// The log event to add + /// true if the log was added, false if the log was unable to be added + bool AddLogEvent(LogEventWireModel logEvent); } } diff --git a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs index df61098974..40c484f32d 100644 --- a/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs +++ b/src/Agent/NewRelic/Agent/Core/Transactions/Transaction.cs @@ -867,8 +867,26 @@ public ITransaction AddCustomAttribute(string key, object value) return this; } - private readonly ConcurrentList _logEvents = new ConcurrentList(); - public IList LogEvents { get => _logEvents; } + private ConcurrentList _logEvents = new ConcurrentList(); + + public IList HarvestLogEvents() + { + return Interlocked.Exchange(ref _logEvents, null); + } + + public bool AddLogEvent(LogEventWireModel logEvent) + { + try + { + _logEvents.Add(logEvent); + return true; + } + catch (Exception) + { + // We will hit this if logs have been harvested from the transaction already... + } + return false; + } private readonly ConcurrentList _segments = new ConcurrentList(); public IList Segments { get => _segments; } diff --git a/src/Agent/NewRelic/Agent/Core/Transformers/TransactionTransformer/TransactionTransformer.cs b/src/Agent/NewRelic/Agent/Core/Transformers/TransactionTransformer/TransactionTransformer.cs index 7d91106e2b..2121ed5a8d 100644 --- a/src/Agent/NewRelic/Agent/Core/Transformers/TransactionTransformer/TransactionTransformer.cs +++ b/src/Agent/NewRelic/Agent/Core/Transformers/TransactionTransformer/TransactionTransformer.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using static NewRelic.Agent.Core.WireModels.MetricWireModel; namespace NewRelic.Agent.Core.Transformers.TransactionTransformer @@ -92,7 +93,7 @@ public void Transform(IInternalTransaction transaction) ComputeSampled(transaction); PrioritizeAndCollectLogEvents(transaction); - + var immutableTransaction = transaction.ConvertToImmutableTransaction(); // Note: Metric names are normally handled internally by the IMetricBuilder. However, transactionMetricName is an exception because (sadly) it is used for more than just metrics. For example, transaction events need to use metric name, as does RUM and CAT. @@ -475,7 +476,7 @@ private bool ErrorCollectionEnabled() private void PrioritizeAndCollectLogEvents(IInternalTransaction transaction) { - _logEventAggregator.CollectWithPriority(transaction.LogEvents, transaction.Priority); + _logEventAggregator.CollectWithPriority(transaction.HarvestLogEvents(), transaction.Priority); } } } diff --git a/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteService.cs b/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteService.cs index c025ba5635..eff00575db 100644 --- a/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteService.cs +++ b/tests/Agent/IntegrationTests/IntegrationTestHelpers/RemoteServiceFixtures/RemoteService.cs @@ -218,6 +218,7 @@ public override void Start(string commandLineArguments, Dictionary _configurationService.Configuration.LogEventCollectorEnabled) + .Returns(true); + + var timestamp = DateTime.Now; + var timestampUnix = timestamp.ToUnixTimeMilliseconds(); + var level = "DEBUG"; + var message = "message"; + + Func getLevelFunc = (l) => level; + Func getTimestampFunc = (l) => timestamp; + Func getMessageFunc = (l) => message; + + var spanId = "spanid"; + var traceId = "traceid"; + var loggingFramework = "testFramework"; + + SetupTransaction(); + var transaction = _transactionService.GetCurrentInternalTransaction(); + var priority = transaction.Priority; + transaction.HarvestLogEvents(); + + var xapi = _agent as IAgentExperimental; + xapi.RecordLogMessage(loggingFramework, new object(), getTimestampFunc, getLevelFunc, getMessageFunc, spanId, traceId); + + var privateAccessor = new PrivateAccessor(_logEventAggregator); + var logEvents = privateAccessor.GetField("_logEvents") as ConcurrentPriorityQueue>; + + var logEvent = logEvents?.FirstOrDefault()?.Data; + Assert.AreEqual(1, logEvents.Count); Assert.IsNotNull(logEvent); Assert.AreEqual(timestampUnix, logEvent.TimeStamp); Assert.AreEqual(level, logEvent.Level);