Skip to content

Commit 81abc5c

Browse files
authored
feat:Add a new API method to time currently unsupported datastore method calls. (#2320)
1 parent a4c5994 commit 81abc5c

File tree

10 files changed

+375
-5
lines changed

10 files changed

+375
-5
lines changed

src/Agent/NewRelic.Api.Agent/ITransaction.cs

+15
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,20 @@ public interface ITransaction
8080
/// </summary>
8181
/// <param name="userid">The User Id for this transaction.</param>
8282
void SetUserId(string userid);
83+
84+
/// <summary>
85+
/// Records a datastore segment.
86+
/// This function allows an unsupported datastore to be instrumented in the same way as the .NET agent automatically instruments its supported datastores.
87+
/// </summary>
88+
/// <param name="vendor">Datastore vendor name, for example MySQL, MSSQL, MongoDB.</param>
89+
/// <param name="model">Table name or similar in non-relational datastores.</param>
90+
/// <param name="operation">Operation being performed, for example "SELECT" or "UPDATE" for SQL databases.</param>
91+
/// <param name="commandText">Optional. Query or similar in non-relational datastores.</param>
92+
/// <param name="host">Optional. Server hosting the datastore</param>
93+
/// <param name="portPathOrID">Optional. Port, path or other ID to aid in identifying the datastore.</param>
94+
/// <param name="databaseName">Optional. Datastore name.</param>
95+
/// <returns>IDisposable segment wrapper that both creates and ends the segment automatically.</returns>
96+
SegmentWrapper? RecordDatastoreSegment(string vendor, string model, string operation,
97+
string? commandText = null, string? host = null, string? portPathOrID = null, string? databaseName = null);
8398
}
8499
}

src/Agent/NewRelic.Api.Agent/NoOpTransaction.cs

+6
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,11 @@ public void AcceptDistributedTraceHeaders<T>(T carrier, Func<T, string, IEnumera
2828
public void SetUserId(string userid)
2929
{
3030
}
31+
32+
public SegmentWrapper? RecordDatastoreSegment(string vendor, string model, string operation,
33+
string? commandText, string? host, string? portPathOrID, string? databaseName)
34+
{
35+
return null;
36+
}
3137
}
3238
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System;
5+
6+
namespace NewRelic.Api.Agent
7+
{
8+
public class SegmentWrapper : IDisposable
9+
{
10+
private volatile dynamic _segment;
11+
12+
public static SegmentWrapper GetDatastoreWrapper(dynamic transaction,
13+
string vendor, string model, string operation,
14+
string? commandText, string? host, string? portPathOrID, string? databaseName)
15+
{
16+
return new SegmentWrapper(transaction.StartDatastoreSegment(vendor, model, operation,
17+
commandText, host, portPathOrID, databaseName));
18+
}
19+
20+
private SegmentWrapper(dynamic segment)
21+
{
22+
_segment = segment;
23+
}
24+
25+
public void Dispose()
26+
{
27+
_segment.End();
28+
}
29+
}
30+
}

src/Agent/NewRelic.Api.Agent/Transaction.cs

+34
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,39 @@ public void SetUserId(string userid)
118118
_isSetUserIdAvailable = false;
119119
}
120120
}
121+
122+
private static bool _isCreateDatastoreSegmentAvailable = true;
123+
/// <summary>
124+
/// Records a datastore segment.
125+
/// This function allows an unsupported datastore to be instrumented in the same way as the .NET agent automatically instruments its supported datastores.
126+
/// </summary>
127+
/// <param name="vendor">Datastore vendor name, for example MySQL, MSSQL, MongoDB.</param>
128+
/// <param name="model">Table name or similar in non-relational datastores.</param>
129+
/// <param name="operation">Operation being performed, for example "SELECT" or "UPDATE" for SQL databases.</param>
130+
/// <param name="commandText">Optional. Query or similar in non-relational datastores.</param>
131+
/// <param name="host">Optional. Server hosting the datastore</param>
132+
/// <param name="portPathOrID">Optional. Port, path or other ID to aid in identifying the datastore.</param>
133+
/// <param name="databaseName">Optional. Datastore name.</param>
134+
/// <returns>IDisposable segment wrapper that both creates and ends the segment automatically.</returns>
135+
public SegmentWrapper? RecordDatastoreSegment(string vendor, string model, string operation,
136+
string? commandText, string? host, string? portPathOrID, string? databaseName)
137+
{
138+
if (!_isCreateDatastoreSegmentAvailable)
139+
{
140+
return null;
141+
}
142+
143+
try
144+
{
145+
return SegmentWrapper.GetDatastoreWrapper(_wrappedTransaction, vendor, model, operation,
146+
commandText, host, portPathOrID, databaseName);
147+
}
148+
catch (RuntimeBinderException)
149+
{
150+
_isCreateDatastoreSegmentAvailable = false;
151+
}
152+
153+
return null;
154+
}
121155
}
122156
}

src/Agent/NewRelic/Agent/Core/Api/TransactionBridgeApi.cs

+49-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using System;
5+
using System.Collections.Generic;
56
using NewRelic.Agent.Api;
6-
using NewRelic.Agent.Core.Metrics;
77
using NewRelic.Agent.Configuration;
8+
using NewRelic.Agent.Core.Metrics;
9+
using NewRelic.Agent.Extensions.Parsing;
810
using NewRelic.Agent.Extensions.Providers.Wrapper;
911
using NewRelic.Core.Logging;
10-
using System.Collections.Generic;
1112

1213
namespace NewRelic.Agent.Core.Api
1314
{
@@ -203,5 +204,51 @@ public void SetUserId(string userid)
203204
}
204205
}
205206
}
207+
208+
/// <summary>
209+
/// Records a datastore segment.
210+
/// This function allows an unsupported datastore to be instrumented in the same way as the .NET agent automatically instruments its supported datastores.
211+
/// </summary>
212+
/// <param name="vendor">Datastore vendor name, for example MySQL, MSSQL, MongoDB.</param>
213+
/// <param name="model">Table name or similar in non-relational datastores.</param>
214+
/// <param name="operation">Operation being performed, for example "SELECT" or "UPDATE" for SQL databases.</param>
215+
/// <param name="commandText">Optional. Query or similar in non-relational datastores.</param>
216+
/// <param name="host">Optional. Server hosting the datastore</param>
217+
/// <param name="portPathOrID">Optional. Port, path or other ID to aid in identifying the datastore.</param>
218+
/// <param name="databaseName">Optional. Datastore name.</param>
219+
/// <returns>Segment that was created.</returns>
220+
public ISegment StartDatastoreSegment(string vendor, string model, string operation,
221+
string commandText = null, string host = null, string portPathOrID = null, string databaseName = null)
222+
{
223+
try
224+
{
225+
_apiSupportabilityMetricCounters.Record(ApiMethod.StartDatastoreSegment);
226+
var method = new Method(typeof(object), "StartDatastoreSegment", string.Empty);
227+
var methodCall = new MethodCall(method, null, null, false);
228+
var parsedSqlStatement = new ParsedSqlStatement(DatastoreVendor.Other, model, operation);
229+
var connectionInfo = new ConnectionInfo(vendor.ToLower(), host, portPathOrID, databaseName);
230+
return _transaction.StartDatastoreSegment(
231+
methodCall: methodCall,
232+
parsedSqlStatement: parsedSqlStatement,
233+
connectionInfo: connectionInfo,
234+
commandText: commandText,
235+
queryParameters: null,
236+
isLeaf: false
237+
);
238+
}
239+
catch (Exception ex)
240+
{
241+
try
242+
{
243+
Log.Error(ex, "Error in StartDatastoreSegment");
244+
}
245+
catch (Exception)
246+
{
247+
//Swallow the error
248+
}
249+
}
250+
251+
return null;
252+
}
206253
}
207254
}

src/Agent/NewRelic/Agent/Core/Metrics/ApiSupportabilityMetricCounters.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public enum ApiMethod
3636
AcceptDistributedTraceHeaders = 22,
3737
SpanSetName = 23,
3838
SetErrorGroupCallback = 24,
39-
SetUserId = 25
39+
SetUserId = 25,
40+
StartDatastoreSegment = 26
4041
}
4142

4243
public interface IApiSupportabilityMetricCounters : IOutOfBandMetricSource

tests/Agent/IntegrationTests/IntegrationTests/Api/ApiCallsTests.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ public abstract class ApiCallsTests<TFixture> : NewRelicIntegrationTest<TFixture
3333
private readonly string[] ApiCalls = new string[]
3434
{
3535
"TestTraceMetadata",
36-
"TestGetLinkingMetadata"
36+
"TestGetLinkingMetadata",
37+
"TestFullRecordDatastoreSegment",
38+
"TestRequiredRecordDatastoreSegment"
3739
};
3840

3941
protected readonly TFixture Fixture;
@@ -68,7 +70,8 @@ public void ExpectedMetrics()
6870
var expectedMetrics = new List<Assertions.ExpectedMetric>
6971
{
7072
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/TraceMetadata" },
71-
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/GetLinkingMetadata"}
73+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/GetLinkingMetadata"},
74+
new Assertions.ExpectedMetric(){ callCount = 2, metricName = "Supportability/ApiInvocation/StartDatastoreSegment"}
7275
};
7376

7477
var actualMetrics = Fixture.AgentLog.GetMetrics().ToList();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using NewRelic.Agent.IntegrationTestHelpers;
8+
using NewRelic.Agent.IntegrationTestHelpers.RemoteServiceFixtures;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
namespace NewRelic.Agent.IntegrationTests.Api
13+
{
14+
[NetFrameworkTest]
15+
public class RecordDatastoreSegment_Full_TestsFWLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureFWLatest>
16+
{
17+
public RecordDatastoreSegment_Full_TestsFWLatest(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output)
18+
: base(fixture, output, true)
19+
{
20+
}
21+
}
22+
23+
[NetFrameworkTest]
24+
public class RecordDatastoreSegment_RequiredOnly_TestsFWLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureFWLatest>
25+
{
26+
public RecordDatastoreSegment_RequiredOnly_TestsFWLatest(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output)
27+
: base(fixture, output, false)
28+
{
29+
}
30+
}
31+
32+
[NetCoreTest]
33+
public class RecordDatastoreSegment_Full_TestsCoreLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureCoreLatest>
34+
{
35+
public RecordDatastoreSegment_Full_TestsCoreLatest(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output)
36+
: base(fixture, output, true)
37+
{
38+
}
39+
}
40+
41+
[NetCoreTest]
42+
public class RecordDatastoreSegment_RequiredOnly_TestsCoreLatest : RecordDatastoreSegmentTests<ConsoleDynamicMethodFixtureCoreLatest>
43+
{
44+
public RecordDatastoreSegment_RequiredOnly_TestsCoreLatest(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output)
45+
: base(fixture, output, false)
46+
{
47+
}
48+
}
49+
50+
public abstract class RecordDatastoreSegmentTests<TFixture> : NewRelicIntegrationTest<TFixture> where TFixture : ConsoleDynamicMethodFixture
51+
{
52+
protected readonly TFixture _fixture;
53+
54+
private bool _allOptions;
55+
56+
public RecordDatastoreSegmentTests(TFixture fixture, ITestOutputHelper output, bool allOptions) : base(fixture)
57+
{
58+
_fixture = fixture;
59+
_fixture.TestLogger = output;
60+
_allOptions = allOptions;
61+
62+
if(_allOptions)
63+
{
64+
_fixture.AddCommand("ApiCalls TestFullRecordDatastoreSegment");
65+
}
66+
else
67+
{
68+
_fixture.AddCommand("ApiCalls TestRequiredRecordDatastoreSegment");
69+
}
70+
71+
_fixture.Actions
72+
(
73+
setupConfiguration: () =>
74+
{
75+
var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath);
76+
configModifier.SetOrDeleteDistributedTraceEnabled(true);
77+
configModifier.SetLogLevel("finest");
78+
configModifier.DisableEventListenerSamplers(); // Required for .NET 8 to pass.
79+
configModifier.ConfigureFasterMetricsHarvestCycle(25);
80+
configModifier.ConfigureFasterSqlTracesHarvestCycle(30);
81+
}
82+
);
83+
84+
_fixture.AddActions
85+
(
86+
exerciseApplication: () =>
87+
{
88+
var threadProfileMatch = _fixture.AgentLog.WaitForLogLine(AgentLogFile.SqlTraceDataLogLineRegex, TimeSpan.FromMinutes(1));
89+
}
90+
);
91+
92+
_fixture.Initialize();
93+
}
94+
95+
[Fact]
96+
public void Test()
97+
{
98+
var expectedMetrics = new List<Assertions.ExpectedMetric>
99+
{
100+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Supportability/ApiInvocation/StartDatastoreSegment" },
101+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/statement/Other/MyModel/MyOperation" },
102+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/operation/Other/MyOperation" },
103+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/all" },
104+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/allOther" },
105+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/Other/all" },
106+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/Other/allOther" },
107+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = _allOptions ? "Datastore/instance/Other/MyHost/MyPath" : "Datastore/instance/Other/unknown/unknown" },
108+
new Assertions.ExpectedMetric(){ callCount = 1, metricName = "Datastore/statement/Other/MyModel/MyOperation", metricScope = "OtherTransaction/Custom/MultiFunctionApplicationHelpers.Libraries.ApiCalls/RecordDatastoreSegment" },
109+
};
110+
111+
// this will not exist if command text is missing.
112+
var expectedSqlTraces = new List<Assertions.ExpectedSqlTrace>
113+
{
114+
new Assertions.ExpectedSqlTrace()
115+
{
116+
Sql = "MyCommandText",
117+
DatastoreMetricName = "Datastore/statement/Other/MyModel/MyOperation",
118+
TransactionName = "OtherTransaction/Custom/MultiFunctionApplicationHelpers.Libraries.ApiCalls/RecordDatastoreSegment",
119+
HasExplainPlan = false
120+
}
121+
};
122+
123+
var actualMetrics = _fixture.AgentLog.GetMetrics().ToList();
124+
125+
var actualSqlTraces = _fixture.AgentLog.GetSqlTraces().ToList(); //0
126+
127+
Assertions.MetricsExist(expectedMetrics, actualMetrics);
128+
129+
if (_allOptions)
130+
{
131+
Assertions.SqlTraceExists(expectedSqlTraces, actualSqlTraces);
132+
}
133+
else // RequiredOnly
134+
{
135+
Assert.True(actualSqlTraces.Count == 0);
136+
}
137+
138+
}
139+
}
140+
}

tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/ApiCalls.cs

+32
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,37 @@ public static void TestSetTransactionName(string category, string names)
100100
}
101101
}
102102

103+
[Transaction]
104+
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
105+
public static void RecordDatastoreSegment(string vendor, string model, string operation,
106+
string commandText = null, string host = null, string portPathOrID = null, string databaseName = null)
107+
{
108+
var transaction = NewRelic.Api.Agent.NewRelic.GetAgent().CurrentTransaction;
109+
using (transaction.RecordDatastoreSegment(vendor, model, operation,
110+
commandText, host, portPathOrID, databaseName))
111+
{
112+
DatastoreWorker();
113+
}
114+
}
115+
116+
private static void DatastoreWorker()
117+
{
118+
System.Threading.Thread.Sleep(1000);
119+
}
120+
121+
[LibraryMethod]
122+
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
123+
public static void TestFullRecordDatastoreSegment()
124+
{
125+
RecordDatastoreSegment("MyVendor", "MyModel", "MyOperation",
126+
"MyCommandText", "MyHost", "MyPath", "MyDatabase");
127+
}
128+
129+
[LibraryMethod]
130+
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
131+
public static void TestRequiredRecordDatastoreSegment()
132+
{
133+
RecordDatastoreSegment("MyVendor", "MyModel", "MyOperation");
134+
}
103135
}
104136
}

0 commit comments

Comments
 (0)