Skip to content

Commit 4309938

Browse files
authored
feat: Add support for instrumenting OpenSearchClient requests. (#2956)
1 parent 74c0f8d commit 4309938

File tree

19 files changed

+1471
-290
lines changed

19 files changed

+1471
-290
lines changed

.github/workflows/scripts/nugetSlackNotifications/packageInfo.json

+3
Original file line numberDiff line numberDiff line change
@@ -156,5 +156,8 @@
156156
},
157157
{
158158
"packageName": "stackexchange.redis"
159+
},
160+
{
161+
"packageName": "opensearch.client"
159162
}
160163
]

FullAgent.sln

+9-1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Home", "src\Agent\NewRelic\
177177
{D9428449-3E4B-4723-A8AA-1191315C7AAD} = {D9428449-3E4B-4723-A8AA-1191315C7AAD}
178178
{E10BF2F9-D5CA-4330-8169-ED30D861697E} = {E10BF2F9-D5CA-4330-8169-ED30D861697E}
179179
{EA98ED03-D1B4-4283-8412-98985B06AFDA} = {EA98ED03-D1B4-4283-8412-98985B06AFDA}
180+
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD} = {EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}
180181
{EC34F023-223D-432F-9401-9C3ED1B75DE4} = {EC34F023-223D-432F-9401-9C3ED1B75DE4}
181182
{EFFD9051-E3AC-4266-9AF6-7ECC74C032BD} = {EFFD9051-E3AC-4266-9AF6-7ECC74C032BD}
182183
{F889CE37-934F-48F2-A105-6C19CE292D37} = {F889CE37-934F-48F2-A105-6C19CE292D37}
@@ -222,6 +223,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicApiChangeTests", "tes
222223
EndProject
223224
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memcached", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\Memcached\Memcached.csproj", "{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}"
224225
EndProject
226+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSearch", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\OpenSearch\OpenSearch.csproj", "{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}"
227+
EndProject
225228
Global
226229
GlobalSection(SolutionConfigurationPlatforms) = preSolution
227230
Debug|Any CPU = Debug|Any CPU
@@ -468,6 +471,10 @@ Global
468471
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Debug|Any CPU.Build.0 = Debug|Any CPU
469472
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Release|Any CPU.ActiveCfg = Release|Any CPU
470473
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Release|Any CPU.Build.0 = Release|Any CPU
474+
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
475+
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
476+
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
477+
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Release|Any CPU.Build.0 = Release|Any CPU
471478
EndGlobalSection
472479
GlobalSection(SolutionProperties) = preSolution
473480
HideSolutionNode = FALSE
@@ -539,10 +546,11 @@ Global
539546
{338AD83A-ED68-438A-8FB1-E93A3AE87EA8} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
540547
{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2} = {E5B988C0-5D19-407E-8210-71FFB90C579A}
541548
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
549+
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
542550
EndGlobalSection
543551
GlobalSection(ExtensibilityGlobals) = postSolution
544-
EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35
545552
SolutionGuid = {D8B98070-6B8E-403C-A07F-A3F2E4A3A3D0}
553+
EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35
546554
EndGlobalSection
547555
GlobalSection(TestCaseManagementSettings) = postSolution
548556
CategoryFile = FullAgent.vsmdi

build/ArtifactBuilder/CoreAgentComponents.cs

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ protected override void CreateAgentComponents()
6060
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
6161
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
6262
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
63+
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.dll",
6364
};
6465

6566
var wrapperXmls = new[]
@@ -88,6 +89,7 @@ protected override void CreateAgentComponents()
8889
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
8990
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
9091
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml",
92+
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml",
9193
};
9294

9395
ExtensionXsd = $@"{SourceHomeBuilderPath}\extensions\extension.xsd";

build/ArtifactBuilder/FrameworkAgentComponents.cs

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ protected override void CreateAgentComponents()
6767
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
6868
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
6969
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
70+
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.dll",
7071
};
7172

7273
var wrapperXmls = new[]
@@ -109,6 +110,7 @@ protected override void CreateAgentComponents()
109110
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
110111
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
111112
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml",
113+
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml",
112114
};
113115

114116
ExtensionXsd = $@"{SourceHomeBuilderPath}\extensions\extension.xsd";

src/Agent/MsiInstaller/Installer/Product.wxs

+12
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,9 @@ SPDX-License-Identifier: Apache-2.0
404404
<Component Id="MemcachedWrapperComponent" Guid="{2FF15179-BBEB-460C-A145-10F20C0CAD07}">
405405
<File Id="MemcachednWrapperFile" Name="NewRelic.Providers.Wrapper.Memcached.dll" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.Memcached.dll" />
406406
</Component>
407+
<Component Id="OpenSearchWrapperComponent" Guid="{7C996233-F71E-48C2-B389-82F0E0CAB002}">
408+
<File Id="OpenSearchWrapperFile" Name="NewRelic.Providers.Wrapper.OpenSearch.dll" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.OpenSearch.dll" />
409+
</Component>
407410
</ComponentGroup>
408411

409412
<ComponentGroup Id="CoreNewRelic.Agent.Extensions" Directory="CoreProgramFilesExtensionsFolder">
@@ -482,6 +485,9 @@ SPDX-License-Identifier: Apache-2.0
482485
<Component Id="CoreMemcachedWrapperComponent" Guid="{1D7D04A1-24D5-4716-B7CB-EACB21D66D7D}">
483486
<File Id="CoreMemcachedWrapperFile" Name="NewRelic.Providers.Wrapper.Memcached.dll" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.Memcached.dll" />
484487
</Component>
488+
<Component Id="CoreOpenSearchWrapperComponent" Guid="{C04FA73A-3232-409E-9E82-7CD27C18845D}">
489+
<File Id="CoreOpenSearchWrapperFile" Name="NewRelic.Providers.Wrapper.OpenSearch.dll" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.OpenSearch.dll" />
490+
</Component>
485491
</ComponentGroup>
486492

487493
<!-- Wrapper Instrumentation Files-->
@@ -600,6 +606,9 @@ SPDX-License-Identifier: Apache-2.0
600606
<Component Id="MemcachedInstrumentationComponent" Guid="{065F899F-4942-43C6-9589-538C432E3E4D}">
601607
<File Id="MemcachedInstrumentationFile" Name="NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml" />
602608
</Component>
609+
<Component Id="OpenSearchInstrumentationComponent" Guid="{A4ACAB67-AC69-43C5-8FDF-DF3FF6794104}">
610+
<File Id="OpenSearchInstrumentationFile" Name="NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml" />
611+
</Component>
603612
</ComponentGroup>
604613

605614
<ComponentGroup Id="CoreNewRelic.Agent.Extensions.Instrumentation" Directory="CoreExtensionsFolder">
@@ -675,6 +684,9 @@ SPDX-License-Identifier: Apache-2.0
675684
<Component Id="CoreMemcachedInstrumentationComponent" Guid="{5A78488A-837C-4CA5-BD20-4A1ED734C085}">
676685
<File Id="CoreMemcachedInstrumentationFile" Name="NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml"/>
677686
</Component>
687+
<Component Id="CoreOpenSearchInstrumentationComponent" Guid="{E2676B80-D7E2-4DFB-BDAF-75383810B0AB}">
688+
<File Id="CoreOpenSearchInstrumentationFile" Name="NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml" />
689+
</Component>
678690
</ComponentGroup>
679691

680692
<!-- Extensions XSD-->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
7+
using System.Collections.ObjectModel;
8+
using System.Threading.Tasks;
9+
using NewRelic.Agent.Api;
10+
using NewRelic.Agent.Api.Experimental;
11+
using NewRelic.Agent.Extensions.Parsing;
12+
using NewRelic.Agent.Extensions.Providers.Wrapper;
13+
using NewRelic.Agent.Extensions.SystemExtensions;
14+
using NewRelic.Reflection;
15+
16+
namespace NewRelic.Agent.Extensions.Helpers
17+
{
18+
public abstract class SearchRequestWrapperBase
19+
{
20+
private Func<object, bool> _successGetter;
21+
private Func<object, object> _exceptionGetter;
22+
private Func<object, Uri> _uriGetter;
23+
24+
protected ConcurrentDictionary<Type, Func<object, object>> GetRequestResponseFromGeneric = new ConcurrentDictionary<Type, Func<object, object>>();
25+
26+
public abstract DatastoreVendor Vendor { get; }
27+
28+
protected ISegment BuildSegment(int requestParamsIndex, InstrumentedMethodCall instrumentedMethodCall, ITransaction transaction)
29+
{
30+
var path = (string)instrumentedMethodCall.MethodCall.MethodArguments[1];
31+
var request = instrumentedMethodCall.MethodCall.MethodArguments[0].ToString();
32+
var requestParams = instrumentedMethodCall.MethodCall.MethodArguments[requestParamsIndex];
33+
var splitPath = path.Trim('/').Split('/');
34+
35+
var operation = (requestParams == null) ? GetOperationFromPath(request, splitPath) : GetOperationFromRequestParams(requestParams);
36+
37+
var model = splitPath[0]; // For Elastic/OpenSearch model is the index name. This is often the first component of the request path, but not always.
38+
if ((model.Length == 0) || (model[0] == '_')) // Per Elastic/OpenSearch docs, index names aren't allowed to start with an underscore, and the first component of the path can be an operation name in some cases, e.g. "_bulk" or "_msearch"
39+
{
40+
model = "Unknown";
41+
}
42+
43+
var transactionExperimental = transaction.GetExperimentalApi();
44+
var datastoreSegmentData = transactionExperimental.CreateDatastoreSegmentData(new ParsedSqlStatement(Vendor, model, operation), new ConnectionInfo(string.Empty, string.Empty, string.Empty), string.Empty, null);
45+
var segment = transactionExperimental.StartSegment(instrumentedMethodCall.MethodCall);
46+
segment.GetExperimentalApi().SetSegmentData(datastoreSegmentData).MakeLeaf();
47+
48+
return segment;
49+
}
50+
51+
protected void TryProcessResponse(IAgent agent, ITransaction transaction, object response, ISegment segment, Func<object, object> apiCallDetailsGetter)
52+
{
53+
try
54+
{
55+
if (response == null || segment == null)
56+
{
57+
return;
58+
}
59+
var apiCallDetails = apiCallDetailsGetter.Invoke(response);
60+
var uri = GetUriFromApiCallDetails(apiCallDetails);
61+
SetUriOnDatastoreSegment(segment, uri);
62+
ReportError(transaction, apiCallDetails);
63+
64+
segment.End();
65+
}
66+
catch (Exception ex)
67+
{
68+
agent.HandleWrapperException(ex);
69+
}
70+
}
71+
72+
private void ReportError(ITransaction transaction, object apiCallDetails)
73+
{
74+
var exceptionGetter = _exceptionGetter ??= GetExceptionGetterFromApiCallDetails(apiCallDetails);
75+
var ex = exceptionGetter(apiCallDetails);
76+
77+
if ((ex != null) && (ex is Exception exception))
78+
{
79+
transaction.NoticeError(exception);
80+
return;
81+
}
82+
83+
// If an error can be caught by the library before the request is made, it doesn't throw an exception, or
84+
// set any kind of error object. The best we can do is check if it was successful, and use the ToString()
85+
// override to get a summary of what happened
86+
var successGetter = _successGetter ??= GetSuccessGetterFromApiCallDetails(apiCallDetails);
87+
var success = successGetter(apiCallDetails);
88+
89+
if (!success)
90+
{
91+
transaction.NoticeError(new Exception(apiCallDetails.ToString()));
92+
}
93+
}
94+
95+
private Uri GetUriFromApiCallDetails(object apiCallDetails)
96+
{
97+
var UriGetter = _uriGetter ??= GetUriGetterFromApiCallDetails(apiCallDetails);
98+
var uri = UriGetter.Invoke(apiCallDetails);
99+
100+
return uri;
101+
}
102+
103+
private static Func<object, Uri> GetUriGetterFromApiCallDetails(object apiCallDetails)
104+
{
105+
var typeOfApiCall = apiCallDetails.GetType();
106+
var responseAssemblyName = apiCallDetails.GetType().Assembly.FullName;
107+
108+
return VisibilityBypasser.Instance.GeneratePropertyAccessor<Uri>(responseAssemblyName, typeOfApiCall.FullName, "Uri");
109+
}
110+
111+
private static Func<object, Exception> GetExceptionGetterFromApiCallDetails(object apiCallDetails)
112+
{
113+
var typeOfApiCall = apiCallDetails.GetType();
114+
var responseAssemblyName = apiCallDetails.GetType().Assembly.FullName;
115+
116+
return VisibilityBypasser.Instance.GeneratePropertyAccessor<Exception>(responseAssemblyName, typeOfApiCall.FullName, "OriginalException");
117+
}
118+
119+
private static Func<object, bool> GetSuccessGetterFromApiCallDetails(object apiCallDetails)
120+
{
121+
var typeOfApiCall = apiCallDetails.GetType();
122+
var responseAssemblyName = apiCallDetails.GetType().Assembly.FullName;
123+
124+
// "Success" might be better, but it isn't available on all libraries
125+
return VisibilityBypasser.Instance.GeneratePropertyAccessor<bool>(responseAssemblyName, typeOfApiCall.FullName, "SuccessOrKnownError");
126+
}
127+
128+
// Some request types are defined by the HTTP request
129+
private static ReadOnlyDictionary<string, string> RequestMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
130+
{
131+
{ "PUT|_doc", "Index" },
132+
{ "POST|_doc", "Index" },
133+
{ "GET|_doc", "Get" },
134+
{ "HEAD|_doc", "Get" },
135+
{ "DELETE|_doc", "Delete" },
136+
});
137+
138+
// Some request types use abbreviations
139+
private static ReadOnlyDictionary<string, string> RenameMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
140+
{
141+
{ "_mget", "MultiGet" },
142+
{ "_termvectors", "TermVectors" },
143+
{ "_msearch", "MultiSearch" },
144+
{ "_mtermvectors", "MultiTermVectors" },
145+
{ "_field_caps", "FieldCapabilities" },
146+
});
147+
148+
// Some request types have a subtype
149+
private static ReadOnlyDictionary<string, string> SubTypeMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
150+
{
151+
{ "_search|template", "SearchTemplate" },
152+
{ "_msearch|template", "MultiSearchTemplate" },
153+
{ "_render|template", "RenderSearchTemplate" },
154+
{ "_search|scroll", "Scroll" },
155+
});
156+
157+
// Some request types depend on the type, subtype, and HTTP request
158+
private static ReadOnlyDictionary<string, string> FullRequestTypeMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
159+
{
160+
{ "DELETE|_search|scroll", "ClearScroll" },
161+
});
162+
163+
private static void ParsePath(string[] splitPath, out string api, out string subType)
164+
{
165+
// Some examples of different structures:
166+
// GET /my-index/_count?q=user:foo => API = "_count"
167+
// GET /my-index/_search => API = "_search"
168+
// PUT /my-index-000001 => API = ""
169+
// GET /_search/scroll => API = "_search", subType = "scroll"
170+
171+
api = "";
172+
subType = "";
173+
bool foundApi = false;
174+
foreach (var path in splitPath)
175+
{
176+
if (string.IsNullOrEmpty(path))
177+
{
178+
continue;
179+
}
180+
// Sub-api is directly after the API
181+
if (foundApi)
182+
{
183+
subType = path.Split('?')[0];
184+
break;
185+
}
186+
else if (path[0] == '_')
187+
{
188+
// The API starts with an underscore and may have parameters
189+
api = path.Split('?')[0];
190+
foundApi = true;
191+
}
192+
}
193+
}
194+
195+
private static string GetOperationFromPath(string request, string[] splitPath)
196+
{
197+
ParsePath(splitPath, out string api, out string subType);
198+
199+
// Since different operations are determined by different combinations of the path, combine the different
200+
// elements into a single string with a separator, so we can do a faster dictionary lookup
201+
string operation;
202+
string apiWithSub = api + "|" + subType;
203+
string apiWithRequest = request + "|" + api;
204+
string fullApi = apiWithRequest + "|" + subType;
205+
206+
// Check from most-specific to least-specific special cases. Most will fall through to the default handler.
207+
if (FullRequestTypeMap.TryGetValue(fullApi, out operation))
208+
{
209+
return operation;
210+
}
211+
if (SubTypeMap.TryGetValue(apiWithSub, out operation))
212+
{
213+
return operation;
214+
}
215+
if (RequestMap.TryGetValue(apiWithRequest, out operation))
216+
{
217+
return operation;
218+
}
219+
if (RenameMap.TryGetValue(api, out operation))
220+
{
221+
return operation;
222+
}
223+
224+
// Many request types are named exactly for their API call, like _search, _create, _search_shards
225+
return api.CapitalizeEachWord('_');
226+
}
227+
228+
protected static string GetOperationFromRequestParams(object requestParams)
229+
{
230+
if (requestParams == null)
231+
{
232+
// Params will be null when the low-level client is used, fall back to a generic operation name
233+
return "Query";
234+
}
235+
var typeOfRequestParams = requestParams.GetType();
236+
237+
var requestParamsTypeName = typeOfRequestParams.Name; // IndexRequestParameters, SearchRequestParameters, etc
238+
return requestParamsTypeName.Remove(requestParamsTypeName.Length - "RequestParameters".Length);
239+
}
240+
241+
private static void SetUriOnDatastoreSegment(ISegment segment, Uri uri)
242+
{
243+
var segmentExperimentalApi = segment.GetExperimentalApi();
244+
var data = segmentExperimentalApi.SegmentData as IDatastoreSegmentData;
245+
data.SetConnectionInfo(new ConnectionInfo(uri.Host, uri.Port, string.Empty));
246+
segmentExperimentalApi.SetSegmentData(data);
247+
}
248+
249+
protected static bool ValidTaskResponse(Task response)
250+
{
251+
return response?.Status == TaskStatus.RanToCompletion;
252+
}
253+
}
254+
}

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Providers/Wrapper/Constants.cs

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public enum DatastoreVendor
8080
//SQLite,
8181
CosmosDB,
8282
Elasticsearch,
83+
OpenSearch,
8384
DynamoDB,
8485
Other
8586
}

0 commit comments

Comments
 (0)