Skip to content

Commit

Permalink
feat: Add support for instrumenting OpenSearchClient requests. (#2956)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaffinito authored Jan 23, 2025
1 parent 74c0f8d commit 4309938
Show file tree
Hide file tree
Showing 19 changed files with 1,471 additions and 290 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,8 @@
},
{
"packageName": "stackexchange.redis"
},
{
"packageName": "opensearch.client"
}
]
10 changes: 9 additions & 1 deletion FullAgent.sln
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Home", "src\Agent\NewRelic\
{D9428449-3E4B-4723-A8AA-1191315C7AAD} = {D9428449-3E4B-4723-A8AA-1191315C7AAD}
{E10BF2F9-D5CA-4330-8169-ED30D861697E} = {E10BF2F9-D5CA-4330-8169-ED30D861697E}
{EA98ED03-D1B4-4283-8412-98985B06AFDA} = {EA98ED03-D1B4-4283-8412-98985B06AFDA}
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD} = {EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}
{EC34F023-223D-432F-9401-9C3ED1B75DE4} = {EC34F023-223D-432F-9401-9C3ED1B75DE4}
{EFFD9051-E3AC-4266-9AF6-7ECC74C032BD} = {EFFD9051-E3AC-4266-9AF6-7ECC74C032BD}
{F889CE37-934F-48F2-A105-6C19CE292D37} = {F889CE37-934F-48F2-A105-6C19CE292D37}
Expand Down Expand Up @@ -222,6 +223,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicApiChangeTests", "tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Memcached", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\Memcached\Memcached.csproj", "{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSearch", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\OpenSearch\OpenSearch.csproj", "{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -468,6 +471,10 @@ Global
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Release|Any CPU.Build.0 = Release|Any CPU
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -539,10 +546,11 @@ Global
{338AD83A-ED68-438A-8FB1-E93A3AE87EA8} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2} = {E5B988C0-5D19-407E-8210-71FFB90C579A}
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
{EC27FFD7-FAE4-4882-95C4-D3FA60F738BD} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35
SolutionGuid = {D8B98070-6B8E-403C-A07F-A3F2E4A3A3D0}
EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35
EndGlobalSection
GlobalSection(TestCaseManagementSettings) = postSolution
CategoryFile = FullAgent.vsmdi
Expand Down
2 changes: 2 additions & 0 deletions build/ArtifactBuilder/CoreAgentComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.dll",
};

var wrapperXmls = new[]
Expand Down Expand Up @@ -88,6 +89,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml",
};

ExtensionXsd = $@"{SourceHomeBuilderPath}\extensions\extension.xsd";
Expand Down
2 changes: 2 additions & 0 deletions build/ArtifactBuilder/FrameworkAgentComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.dll",
};

var wrapperXmls = new[]
Expand Down Expand Up @@ -109,6 +110,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.OpenSearch.Instrumentation.xml",
};

ExtensionXsd = $@"{SourceHomeBuilderPath}\extensions\extension.xsd";
Expand Down
12 changes: 12 additions & 0 deletions src/Agent/MsiInstaller/Installer/Product.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="MemcachedWrapperComponent" Guid="{2FF15179-BBEB-460C-A145-10F20C0CAD07}">
<File Id="MemcachednWrapperFile" Name="NewRelic.Providers.Wrapper.Memcached.dll" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.Memcached.dll" />
</Component>
<Component Id="OpenSearchWrapperComponent" Guid="{7C996233-F71E-48C2-B389-82F0E0CAB002}">
<File Id="OpenSearchWrapperFile" Name="NewRelic.Providers.Wrapper.OpenSearch.dll" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.OpenSearch.dll" />
</Component>
</ComponentGroup>

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

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

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

<!-- Extensions XSD-->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Api.Experimental;
using NewRelic.Agent.Extensions.Parsing;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Agent.Extensions.SystemExtensions;
using NewRelic.Reflection;

namespace NewRelic.Agent.Extensions.Helpers
{
public abstract class SearchRequestWrapperBase
{
private Func<object, bool> _successGetter;
private Func<object, object> _exceptionGetter;
private Func<object, Uri> _uriGetter;

protected ConcurrentDictionary<Type, Func<object, object>> GetRequestResponseFromGeneric = new ConcurrentDictionary<Type, Func<object, object>>();

public abstract DatastoreVendor Vendor { get; }

protected ISegment BuildSegment(int requestParamsIndex, InstrumentedMethodCall instrumentedMethodCall, ITransaction transaction)
{
var path = (string)instrumentedMethodCall.MethodCall.MethodArguments[1];
var request = instrumentedMethodCall.MethodCall.MethodArguments[0].ToString();
var requestParams = instrumentedMethodCall.MethodCall.MethodArguments[requestParamsIndex];
var splitPath = path.Trim('/').Split('/');

var operation = (requestParams == null) ? GetOperationFromPath(request, splitPath) : GetOperationFromRequestParams(requestParams);

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.
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"
{
model = "Unknown";
}

var transactionExperimental = transaction.GetExperimentalApi();
var datastoreSegmentData = transactionExperimental.CreateDatastoreSegmentData(new ParsedSqlStatement(Vendor, model, operation), new ConnectionInfo(string.Empty, string.Empty, string.Empty), string.Empty, null);
var segment = transactionExperimental.StartSegment(instrumentedMethodCall.MethodCall);
segment.GetExperimentalApi().SetSegmentData(datastoreSegmentData).MakeLeaf();

return segment;
}

protected void TryProcessResponse(IAgent agent, ITransaction transaction, object response, ISegment segment, Func<object, object> apiCallDetailsGetter)
{
try
{
if (response == null || segment == null)
{
return;
}
var apiCallDetails = apiCallDetailsGetter.Invoke(response);
var uri = GetUriFromApiCallDetails(apiCallDetails);
SetUriOnDatastoreSegment(segment, uri);
ReportError(transaction, apiCallDetails);

segment.End();
}
catch (Exception ex)
{
agent.HandleWrapperException(ex);
}
}

private void ReportError(ITransaction transaction, object apiCallDetails)
{
var exceptionGetter = _exceptionGetter ??= GetExceptionGetterFromApiCallDetails(apiCallDetails);
var ex = exceptionGetter(apiCallDetails);

if ((ex != null) && (ex is Exception exception))
{
transaction.NoticeError(exception);
return;
}

// If an error can be caught by the library before the request is made, it doesn't throw an exception, or
// set any kind of error object. The best we can do is check if it was successful, and use the ToString()
// override to get a summary of what happened
var successGetter = _successGetter ??= GetSuccessGetterFromApiCallDetails(apiCallDetails);
var success = successGetter(apiCallDetails);

if (!success)
{
transaction.NoticeError(new Exception(apiCallDetails.ToString()));
}
}

private Uri GetUriFromApiCallDetails(object apiCallDetails)
{
var UriGetter = _uriGetter ??= GetUriGetterFromApiCallDetails(apiCallDetails);
var uri = UriGetter.Invoke(apiCallDetails);

return uri;
}

private static Func<object, Uri> GetUriGetterFromApiCallDetails(object apiCallDetails)
{
var typeOfApiCall = apiCallDetails.GetType();
var responseAssemblyName = apiCallDetails.GetType().Assembly.FullName;

return VisibilityBypasser.Instance.GeneratePropertyAccessor<Uri>(responseAssemblyName, typeOfApiCall.FullName, "Uri");
}

private static Func<object, Exception> GetExceptionGetterFromApiCallDetails(object apiCallDetails)
{
var typeOfApiCall = apiCallDetails.GetType();
var responseAssemblyName = apiCallDetails.GetType().Assembly.FullName;

return VisibilityBypasser.Instance.GeneratePropertyAccessor<Exception>(responseAssemblyName, typeOfApiCall.FullName, "OriginalException");
}

private static Func<object, bool> GetSuccessGetterFromApiCallDetails(object apiCallDetails)
{
var typeOfApiCall = apiCallDetails.GetType();
var responseAssemblyName = apiCallDetails.GetType().Assembly.FullName;

// "Success" might be better, but it isn't available on all libraries
return VisibilityBypasser.Instance.GeneratePropertyAccessor<bool>(responseAssemblyName, typeOfApiCall.FullName, "SuccessOrKnownError");
}

// Some request types are defined by the HTTP request
private static ReadOnlyDictionary<string, string> RequestMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
{
{ "PUT|_doc", "Index" },
{ "POST|_doc", "Index" },
{ "GET|_doc", "Get" },
{ "HEAD|_doc", "Get" },
{ "DELETE|_doc", "Delete" },
});

// Some request types use abbreviations
private static ReadOnlyDictionary<string, string> RenameMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
{
{ "_mget", "MultiGet" },
{ "_termvectors", "TermVectors" },
{ "_msearch", "MultiSearch" },
{ "_mtermvectors", "MultiTermVectors" },
{ "_field_caps", "FieldCapabilities" },
});

// Some request types have a subtype
private static ReadOnlyDictionary<string, string> SubTypeMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
{
{ "_search|template", "SearchTemplate" },
{ "_msearch|template", "MultiSearchTemplate" },
{ "_render|template", "RenderSearchTemplate" },
{ "_search|scroll", "Scroll" },
});

// Some request types depend on the type, subtype, and HTTP request
private static ReadOnlyDictionary<string, string> FullRequestTypeMap = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
{
{ "DELETE|_search|scroll", "ClearScroll" },
});

private static void ParsePath(string[] splitPath, out string api, out string subType)
{
// Some examples of different structures:
// GET /my-index/_count?q=user:foo => API = "_count"
// GET /my-index/_search => API = "_search"
// PUT /my-index-000001 => API = ""
// GET /_search/scroll => API = "_search", subType = "scroll"

api = "";
subType = "";
bool foundApi = false;
foreach (var path in splitPath)
{
if (string.IsNullOrEmpty(path))
{
continue;
}
// Sub-api is directly after the API
if (foundApi)
{
subType = path.Split('?')[0];
break;
}
else if (path[0] == '_')
{
// The API starts with an underscore and may have parameters
api = path.Split('?')[0];
foundApi = true;
}
}
}

private static string GetOperationFromPath(string request, string[] splitPath)
{
ParsePath(splitPath, out string api, out string subType);

// Since different operations are determined by different combinations of the path, combine the different
// elements into a single string with a separator, so we can do a faster dictionary lookup
string operation;
string apiWithSub = api + "|" + subType;
string apiWithRequest = request + "|" + api;
string fullApi = apiWithRequest + "|" + subType;

// Check from most-specific to least-specific special cases. Most will fall through to the default handler.
if (FullRequestTypeMap.TryGetValue(fullApi, out operation))
{
return operation;
}
if (SubTypeMap.TryGetValue(apiWithSub, out operation))
{
return operation;
}
if (RequestMap.TryGetValue(apiWithRequest, out operation))
{
return operation;
}
if (RenameMap.TryGetValue(api, out operation))
{
return operation;
}

// Many request types are named exactly for their API call, like _search, _create, _search_shards
return api.CapitalizeEachWord('_');
}

protected static string GetOperationFromRequestParams(object requestParams)
{
if (requestParams == null)
{
// Params will be null when the low-level client is used, fall back to a generic operation name
return "Query";
}
var typeOfRequestParams = requestParams.GetType();

var requestParamsTypeName = typeOfRequestParams.Name; // IndexRequestParameters, SearchRequestParameters, etc
return requestParamsTypeName.Remove(requestParamsTypeName.Length - "RequestParameters".Length);
}

private static void SetUriOnDatastoreSegment(ISegment segment, Uri uri)
{
var segmentExperimentalApi = segment.GetExperimentalApi();
var data = segmentExperimentalApi.SegmentData as IDatastoreSegmentData;
data.SetConnectionInfo(new ConnectionInfo(uri.Host, uri.Port, string.Empty));
segmentExperimentalApi.SetSegmentData(data);
}

protected static bool ValidTaskResponse(Task response)
{
return response?.Status == TaskStatus.RanToCompletion;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public enum DatastoreVendor
//SQLite,
CosmosDB,
Elasticsearch,
OpenSearch,
DynamoDB,
Other
}
Expand Down
Loading

0 comments on commit 4309938

Please sign in to comment.