Skip to content

Commit

Permalink
feat:Add a new API method to time currently unsupported datastore met…
Browse files Browse the repository at this point in the history
…hod calls. (#2320)
  • Loading branch information
jaffinito committed Mar 8, 2024
1 parent a4c5994 commit 81abc5c
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 5 deletions.
15 changes: 15 additions & 0 deletions src/Agent/NewRelic.Api.Agent/ITransaction.cs
Expand Up @@ -80,5 +80,20 @@ public interface ITransaction
/// </summary>
/// <param name="userid">The User Id for this transaction.</param>
void SetUserId(string userid);

/// <summary>
/// Records a datastore segment.
/// This function allows an unsupported datastore to be instrumented in the same way as the .NET agent automatically instruments its supported datastores.
/// </summary>
/// <param name="vendor">Datastore vendor name, for example MySQL, MSSQL, MongoDB.</param>
/// <param name="model">Table name or similar in non-relational datastores.</param>
/// <param name="operation">Operation being performed, for example "SELECT" or "UPDATE" for SQL databases.</param>
/// <param name="commandText">Optional. Query or similar in non-relational datastores.</param>
/// <param name="host">Optional. Server hosting the datastore</param>
/// <param name="portPathOrID">Optional. Port, path or other ID to aid in identifying the datastore.</param>
/// <param name="databaseName">Optional. Datastore name.</param>
/// <returns>IDisposable segment wrapper that both creates and ends the segment automatically.</returns>
SegmentWrapper? RecordDatastoreSegment(string vendor, string model, string operation,
string? commandText = null, string? host = null, string? portPathOrID = null, string? databaseName = null);
}
}
6 changes: 6 additions & 0 deletions src/Agent/NewRelic.Api.Agent/NoOpTransaction.cs
Expand Up @@ -28,5 +28,11 @@ public void AcceptDistributedTraceHeaders<T>(T carrier, Func<T, string, IEnumera
public void SetUserId(string userid)
{
}

public SegmentWrapper? RecordDatastoreSegment(string vendor, string model, string operation,
string? commandText, string? host, string? portPathOrID, string? databaseName)
{
return null;
}
}
}
30 changes: 30 additions & 0 deletions src/Agent/NewRelic.Api.Agent/SegmentWrapper.cs
@@ -0,0 +1,30 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;

namespace NewRelic.Api.Agent
{
public class SegmentWrapper : IDisposable
{
private volatile dynamic _segment;

public static SegmentWrapper GetDatastoreWrapper(dynamic transaction,
string vendor, string model, string operation,
string? commandText, string? host, string? portPathOrID, string? databaseName)
{
return new SegmentWrapper(transaction.StartDatastoreSegment(vendor, model, operation,
commandText, host, portPathOrID, databaseName));
}

private SegmentWrapper(dynamic segment)
{
_segment = segment;
}

public void Dispose()
{
_segment.End();
}
}
}
34 changes: 34 additions & 0 deletions src/Agent/NewRelic.Api.Agent/Transaction.cs
Expand Up @@ -118,5 +118,39 @@ public void SetUserId(string userid)
_isSetUserIdAvailable = false;
}
}

private static bool _isCreateDatastoreSegmentAvailable = true;
/// <summary>
/// Records a datastore segment.
/// This function allows an unsupported datastore to be instrumented in the same way as the .NET agent automatically instruments its supported datastores.
/// </summary>
/// <param name="vendor">Datastore vendor name, for example MySQL, MSSQL, MongoDB.</param>
/// <param name="model">Table name or similar in non-relational datastores.</param>
/// <param name="operation">Operation being performed, for example "SELECT" or "UPDATE" for SQL databases.</param>
/// <param name="commandText">Optional. Query or similar in non-relational datastores.</param>
/// <param name="host">Optional. Server hosting the datastore</param>
/// <param name="portPathOrID">Optional. Port, path or other ID to aid in identifying the datastore.</param>
/// <param name="databaseName">Optional. Datastore name.</param>
/// <returns>IDisposable segment wrapper that both creates and ends the segment automatically.</returns>
public SegmentWrapper? RecordDatastoreSegment(string vendor, string model, string operation,
string? commandText, string? host, string? portPathOrID, string? databaseName)
{
if (!_isCreateDatastoreSegmentAvailable)
{
return null;
}

try
{
return SegmentWrapper.GetDatastoreWrapper(_wrappedTransaction, vendor, model, operation,
commandText, host, portPathOrID, databaseName);
}
catch (RuntimeBinderException)
{
_isCreateDatastoreSegmentAvailable = false;
}

return null;
}
}
}
51 changes: 49 additions & 2 deletions src/Agent/NewRelic/Agent/Core/Api/TransactionBridgeApi.cs
Expand Up @@ -2,12 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using NewRelic.Agent.Api;
using NewRelic.Agent.Core.Metrics;
using NewRelic.Agent.Configuration;
using NewRelic.Agent.Core.Metrics;
using NewRelic.Agent.Extensions.Parsing;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Core.Logging;
using System.Collections.Generic;

namespace NewRelic.Agent.Core.Api
{
Expand Down Expand Up @@ -203,5 +204,51 @@ public void SetUserId(string userid)
}
}
}

/// <summary>
/// Records a datastore segment.
/// This function allows an unsupported datastore to be instrumented in the same way as the .NET agent automatically instruments its supported datastores.
/// </summary>
/// <param name="vendor">Datastore vendor name, for example MySQL, MSSQL, MongoDB.</param>
/// <param name="model">Table name or similar in non-relational datastores.</param>
/// <param name="operation">Operation being performed, for example "SELECT" or "UPDATE" for SQL databases.</param>
/// <param name="commandText">Optional. Query or similar in non-relational datastores.</param>
/// <param name="host">Optional. Server hosting the datastore</param>
/// <param name="portPathOrID">Optional. Port, path or other ID to aid in identifying the datastore.</param>
/// <param name="databaseName">Optional. Datastore name.</param>
/// <returns>Segment that was created.</returns>
public ISegment StartDatastoreSegment(string vendor, string model, string operation,
string commandText = null, string host = null, string portPathOrID = null, string databaseName = null)
{
try
{
_apiSupportabilityMetricCounters.Record(ApiMethod.StartDatastoreSegment);
var method = new Method(typeof(object), "StartDatastoreSegment", string.Empty);
var methodCall = new MethodCall(method, null, null, false);
var parsedSqlStatement = new ParsedSqlStatement(DatastoreVendor.Other, model, operation);
var connectionInfo = new ConnectionInfo(vendor.ToLower(), host, portPathOrID, databaseName);
return _transaction.StartDatastoreSegment(
methodCall: methodCall,
parsedSqlStatement: parsedSqlStatement,
connectionInfo: connectionInfo,
commandText: commandText,
queryParameters: null,
isLeaf: false
);
}
catch (Exception ex)
{
try
{
Log.Error(ex, "Error in StartDatastoreSegment");
}
catch (Exception)
{
//Swallow the error
}
}

return null;
}
}
}
Expand Up @@ -36,7 +36,8 @@ public enum ApiMethod
AcceptDistributedTraceHeaders = 22,
SpanSetName = 23,
SetErrorGroupCallback = 24,
SetUserId = 25
SetUserId = 25,
StartDatastoreSegment = 26
}

public interface IApiSupportabilityMetricCounters : IOutOfBandMetricSource
Expand Down
Expand Up @@ -33,7 +33,9 @@ public abstract class ApiCallsTests<TFixture> : NewRelicIntegrationTest<TFixture
private readonly string[] ApiCalls = new string[]
{
"TestTraceMetadata",
"TestGetLinkingMetadata"
"TestGetLinkingMetadata",
"TestFullRecordDatastoreSegment",
"TestRequiredRecordDatastoreSegment"
};

protected readonly TFixture Fixture;
Expand Down Expand Up @@ -68,7 +70,8 @@ public void ExpectedMetrics()
var expectedMetrics = new List<Assertions.ExpectedMetric>
{
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/TraceMetadata" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/GetLinkingMetadata"}
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/GetLinkingMetadata"},
new Assertions.ExpectedMetric(){ callCount = 2, metricName = "Supportability/ApiInvocation/StartDatastoreSegment"}
};

var actualMetrics = Fixture.AgentLog.GetMetrics().ToList();
Expand Down
@@ -0,0 +1,140 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.Linq;
using NewRelic.Agent.IntegrationTestHelpers;
using NewRelic.Agent.IntegrationTestHelpers.RemoteServiceFixtures;
using Xunit;
using Xunit.Abstractions;

namespace NewRelic.Agent.IntegrationTests.Api
{
[NetFrameworkTest]
public class RecordDatastoreSegment_Full_TestsFWLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureFWLatest>
{
public RecordDatastoreSegment_Full_TestsFWLatest(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output)
: base(fixture, output, true)
{
}
}

[NetFrameworkTest]
public class RecordDatastoreSegment_RequiredOnly_TestsFWLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureFWLatest>
{
public RecordDatastoreSegment_RequiredOnly_TestsFWLatest(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output)
: base(fixture, output, false)
{
}
}

[NetCoreTest]
public class RecordDatastoreSegment_Full_TestsCoreLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureCoreLatest>
{
public RecordDatastoreSegment_Full_TestsCoreLatest(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output)
: base(fixture, output, true)
{
}
}

[NetCoreTest]
public class RecordDatastoreSegment_RequiredOnly_TestsCoreLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureCoreLatest>
{
public RecordDatastoreSegment_RequiredOnly_TestsCoreLatest(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output)
: base(fixture, output, false)
{
}
}

public abstract class RecordDatastoreSegmentTests<TFixture> : NewRelicIntegrationTest<TFixture> where TFixture : ConsoleDynamicMethodFixture
{
protected readonly TFixture _fixture;

private bool _allOptions;

public RecordDatastoreSegmentTests(TFixture fixture, ITestOutputHelper output, bool allOptions) : base(fixture)
{
_fixture = fixture;
_fixture.TestLogger = output;
_allOptions = allOptions;

if(_allOptions)
{
_fixture.AddCommand("ApiCalls TestFullRecordDatastoreSegment");
}
else
{
_fixture.AddCommand("ApiCalls TestRequiredRecordDatastoreSegment");
}

_fixture.Actions
(
setupConfiguration: () =>
{
var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath);
configModifier.SetOrDeleteDistributedTraceEnabled(true);
configModifier.SetLogLevel("finest");
configModifier.DisableEventListenerSamplers(); // Required for .NET 8 to pass.
configModifier.ConfigureFasterMetricsHarvestCycle(25);
configModifier.ConfigureFasterSqlTracesHarvestCycle(30);
}
);

_fixture.AddActions
(
exerciseApplication: () =>
{
var threadProfileMatch = _fixture.AgentLog.WaitForLogLine(AgentLogFile.SqlTraceDataLogLineRegex, TimeSpan.FromMinutes(1));
}
);

_fixture.Initialize();
}

[Fact]
public void Test()
{
var expectedMetrics = new List<Assertions.ExpectedMetric>
{
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/StartDatastoreSegment" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/statement/Other/MyModel/MyOperation" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/operation/Other/MyOperation" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/all" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/allOther" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/Other/all" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/Other/allOther" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = _allOptions ? "Datastore/instance/Other/MyHost/MyPath" : "Datastore/instance/Other/unknown/unknown" },
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/statement/Other/MyModel/MyOperation", metricScope = "OtherTransaction/Custom/MultiFunctionApplicationHelpers.Libraries.ApiCalls/RecordDatastoreSegment" },
};

// this will not exist if command text is missing.
var expectedSqlTraces = new List<Assertions.ExpectedSqlTrace>
{
new Assertions.ExpectedSqlTrace()
{
Sql = "MyCommandText",
DatastoreMetricName = "Datastore/statement/Other/MyModel/MyOperation",
TransactionName = "OtherTransaction/Custom/MultiFunctionApplicationHelpers.Libraries.ApiCalls/RecordDatastoreSegment",
HasExplainPlan = false
}
};

var actualMetrics = _fixture.AgentLog.GetMetrics().ToList();

var actualSqlTraces = _fixture.AgentLog.GetSqlTraces().ToList(); //0

Assertions.MetricsExist(expectedMetrics, actualMetrics);

if (_allOptions)
{
Assertions.SqlTraceExists(expectedSqlTraces, actualSqlTraces);
}
else // RequiredOnly
{
Assert.True(actualSqlTraces.Count == 0);
}

}
}
}
Expand Up @@ -100,5 +100,37 @@ public static void TestSetTransactionName(string category, string names)
}
}

[Transaction]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public static void RecordDatastoreSegment(string vendor, string model, string operation,
string commandText = null, string host = null, string portPathOrID = null, string databaseName = null)
{
var transaction = NewRelic.Api.Agent.NewRelic.GetAgent().CurrentTransaction;
using (transaction.RecordDatastoreSegment(vendor, model, operation,
commandText, host, portPathOrID, databaseName))
{
DatastoreWorker();
}
}

private static void DatastoreWorker()
{
System.Threading.Thread.Sleep(1000);
}

[LibraryMethod]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public static void TestFullRecordDatastoreSegment()
{
RecordDatastoreSegment("MyVendor", "MyModel", "MyOperation",
"MyCommandText", "MyHost", "MyPath", "MyDatabase");
}

[LibraryMethod]
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public static void TestRequiredRecordDatastoreSegment()
{
RecordDatastoreSegment("MyVendor", "MyModel", "MyOperation");
}
}
}

0 comments on commit 81abc5c

Please sign in to comment.