Skip to content

Commit

Permalink
feat: Add support for the EnyimMemcachedCore client. (#2781)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaffinito authored Sep 25, 2024
1 parent f032a9c commit 52bdc11
Show file tree
Hide file tree
Showing 21 changed files with 893 additions and 3 deletions.
13 changes: 10 additions & 3 deletions FullAgent.sln
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSerializationHelpers.Te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AwsSdk", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\AwsSdk\AwsSdk.csproj", "{37262C22-6A3A-4AD7-AB78-3853D2B2931D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureFunction", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\AzureFunction\AzureFunction.csproj", "{338AD83A-ED68-438A-8FB1-E93A3AE87EA8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunction", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\AzureFunction\AzureFunction.csproj", "{338AD83A-ED68-438A-8FB1-E93A3AE87EA8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApiChangeTests", "tests\Agent\UnitTests\PublicApiChangeTests\PublicApiChangeTests.csproj", "{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicApiChangeTests", "tests\Agent\UnitTests\PublicApiChangeTests\PublicApiChangeTests.csproj", "{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memcached", "src\Agent\NewRelic\Agent\Extensions\Providers\Wrapper\Memcached\Memcached.csproj", "{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -461,6 +463,10 @@ Global
{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8F6EFEA-1C31-4461-A7B4-25C30D954EE2}.Release|Any CPU.Build.0 = Release|Any CPU
{5D74E5C5-9BA3-423B-86F7-14C2D1A14661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -531,10 +537,11 @@ Global
{37262C22-6A3A-4AD7-AB78-3853D2B2931D} = {5E86E10A-C38F-48CB-ADE9-67B22BB2F50A}
{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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D8B98070-6B8E-403C-A07F-A3F2E4A3A3D0}
EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.2\lib\NET35
SolutionGuid = {D8B98070-6B8E-403C-A07F-A3F2E4A3A3D0}
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 @@ -59,6 +59,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsLambda.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
};

var wrapperXmls = new[]
Expand Down Expand Up @@ -86,6 +87,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsLambda.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.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 @@ -66,6 +66,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Bedrock.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.dll",
};

var wrapperXmls = new[]
Expand Down Expand Up @@ -107,6 +108,7 @@ protected override void CreateAgentComponents()
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Bedrock.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AwsSdk.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml",
$@"{SourceHomeBuilderPath}\extensions\NewRelic.Providers.Wrapper.Memcached.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 @@ -401,6 +401,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="AzureFunctionWrapperComponent" Guid="{949893C5-0212-447A-88A3-DDE3638DDC6E}">
<File Id="AzureFunctionWrapperFile" Name="NewRelic.Providers.Wrapper.AzureFunction.dll" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll" />
</Component>
<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>
</ComponentGroup>

<ComponentGroup Id="CoreNewRelic.Agent.Extensions" Directory="CoreProgramFilesExtensionsFolder">
Expand Down Expand Up @@ -476,6 +479,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="CoreAzureFunctionWrapperComponent" Guid="{6DC23A07-4063-462E-B43A-532D6F810AFE}">
<File Id="CoreAzureFunctionWrapperFile" Name="NewRelic.Providers.Wrapper.AzureFunction.dll" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.AzureFunction.dll" />
</Component>
<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>
</ComponentGroup>

<!-- Wrapper Instrumentation Files-->
Expand Down Expand Up @@ -591,6 +597,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="AzureFunctionInstrumentationComponent" Guid="{0D802289-3B47-4D94-8907-285FFD2779AB}">
<File Id="AzureFunctionInstrumentationFile" Name="NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml" />
</Component>
<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>
</ComponentGroup>

<ComponentGroup Id="CoreNewRelic.Agent.Extensions.Instrumentation" Directory="CoreExtensionsFolder">
Expand Down Expand Up @@ -663,6 +672,9 @@ SPDX-License-Identifier: Apache-2.0
<Component Id="CoreAzureFunctionInstrumentationComponent" Guid="{BF3BEE66-A563-4A59-ADEC-65C3381C582E}">
<File Id="CoreAzureFunctionInstrumentationFile" Name="NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml" KeyPath="yes" Source="$(var.HomeFolderPath)_coreclr\extensions\NewRelic.Providers.Wrapper.AzureFunction.Instrumentation.xml"/>
</Component>
<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>
</ComponentGroup>

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

using System;
using System.Linq;
using System.Threading.Tasks;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.Parsing;
using NewRelic.Agent.Extensions.Providers.Wrapper;

namespace NewRelic.Providers.Wrapper.Memcached
{
public class EnyimMemcachedCoreWrapper : IWrapper
{
private const string ModelName = "cache";
private const string WrapperName = "EnyimMemcachedCoreWrapper";

public bool IsTransactionRequired => true;

public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
{
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
}

public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
{
if (instrumentedMethodCall.IsAsync)
{
transaction.AttachToAsync();
}

// Internally, the key is used to determine what server to read from in a multi-server environment.
// Without a key, we can't determine the server, so we can't determine the connection info.

ParsedSqlStatement parsedStatement;
string key;

// Operation is the first argument in all cases, Key is the second argument
if (instrumentedMethodCall.MethodCall.Method.MethodName.Equals("PerformStore")
|| instrumentedMethodCall.MethodCall.Method.MethodName.Equals("PerformStoreAsync")
|| instrumentedMethodCall.MethodCall.Method.MethodName.Equals("PerformMutate")
|| instrumentedMethodCall.MethodCall.Method.MethodName.Equals("PerformMutateAsync")
|| instrumentedMethodCall.MethodCall.Method.MethodName.Equals("PerformConcatenate"))
{
key = instrumentedMethodCall.MethodCall.MethodArguments[1].ToString();
parsedStatement = new ParsedSqlStatement(DatastoreVendor.Memcached,
ModelName,
instrumentedMethodCall.MethodCall.MethodArguments[0].ToString());
}
// Operation is always Get, Key is the first argument
else if (instrumentedMethodCall.MethodCall.Method.MethodName.Equals("PerformTryGet")
|| instrumentedMethodCall.MethodCall.Method.MethodName.Equals("PerformGet")
|| instrumentedMethodCall.MethodCall.Method.MethodName.Equals("GetAsync"))
{
key = instrumentedMethodCall.MethodCall.MethodArguments[0].ToString();
parsedStatement = new ParsedSqlStatement(DatastoreVendor.Memcached,
ModelName,
"Get");
}
// Operation is always Remove, Key is the first argument
else if (instrumentedMethodCall.MethodCall.Method.MethodName.Equals("Remove")
|| instrumentedMethodCall.MethodCall.Method.MethodName.Equals("RemoveAsync"))
{
key = instrumentedMethodCall.MethodCall.MethodArguments[0].ToString();
parsedStatement = new ParsedSqlStatement(DatastoreVendor.Memcached,
ModelName,
"Remove");
}
// Should not happen
else
{
return Delegates.NoOp;
}

var connectionInfo = MemcachedHelpers.GetConnectionInfo(
key,
instrumentedMethodCall.MethodCall.InvocationTarget,
agent);

var segment = transaction.StartDatastoreSegment(instrumentedMethodCall.MethodCall, parsedStatement, connectionInfo, isLeaf: true);

if (instrumentedMethodCall.IsAsync)
{
return Delegates.GetAsyncDelegateFor<Task>(
agent,
segment);
}

return Delegates.GetDelegateFor(
onFailure: (ex) => segment.End(ex),
onComplete: () => segment.End()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2020 New Relic Corporation. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->
<extension xmlns="urn:newrelic-extension">
<instrumentation>

<!--Supports all MemcachedClient and "DistributedCache" methods, except MultiX methods.-->
<tracerFactory name="EnyimMemcachedCoreWrapper">

<!--MemcachedClient<T> calls into MemcachedClient-->
<!--DistributedCache is just MemcachedClient, not a separate class-->
<match assemblyName="EnyimMemcachedCore" className="Enyim.Caching.MemcachedClient">
<!--Add/Async, Set/Async, Replace/Async, Store/Async, Cas-->
<exactMethodMatcher methodName="PerformStore" parameters="Enyim.Caching.Memcached.StoreMode,System.String,System.Object,System.UInt32,System.UInt64&amp;,System.Int32&amp;" />
<exactMethodMatcher methodName="PerformStoreAsync" />

<!--Get > TryGet > PerformTryGet-->
<!--GetWithCas > TryGetWithCas > PerformTryGet-->
<!--GetWithCas<T> > TryGetWithCas > PerformTryGet-->
<exactMethodMatcher methodName="PerformTryGet" />

<!--Get<T>-->
<exactMethodMatcher methodName="PerformGet" />

<!--GetValueAsync-->
<exactMethodMatcher methodName="GetAsync" parameters="System.String" />

<!--Increment, Decrement, CasMutate-->
<exactMethodMatcher methodName="PerformMutate" parameters="Enyim.Caching.Memcached.MutationMode,System.String,System.UInt64,System.UInt64,System.UInt32,System.UInt64&amp;" />

<!--TouchAsync-->
<exactMethodMatcher methodName="PerformMutateAsync" />

<!--Append, Prepend-->
<exactMethodMatcher methodName="PerformConcatenate" />

<exactMethodMatcher methodName="Remove" />
<exactMethodMatcher methodName="RemoveAsync" />
</match>
</tracerFactory>

</instrumentation>
</extension>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
<RootNamespace>NewRelic.Providers.Wrapper.Memcached</RootNamespace>
<AssemblyName>NewRelic.Providers.Wrapper.Memcached</AssemblyName>
<Description>Memcached Wrapper Provider for New Relic .NET Agent</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\NewRelic.Agent.Extensions\NewRelic.Agent.Extensions.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="Instrumentation.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using NewRelic.Agent.Api;
using NewRelic.Agent.Extensions.Parsing;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;

namespace NewRelic.Providers.Wrapper.Memcached
{
public class MemcachedHelpers
{
private static bool _hasGetServerFailed = false;
private const string AssemblyName = "EnyimMemcachedCore";
private static Func<object, object> _transformerGetter;
private static Func<object, string, string> _transformMethod;
private static Func<object, object> _poolGetter;
private static Func<object, string, object> _locateMethod;
private static Func<object, object> _endpointGetter;
private static Func<object, object> _addressGetter;
private static Func<object, int> _portGetter;

// To get the ConnectionInfo we need to call the same Transform method that the library calls to get the node.
// This is deterministic based reviewing the code at different versions.
public static ConnectionInfo GetConnectionInfo(string key, object target, IAgent agent)
{
if (_hasGetServerFailed)
{
return new ConnectionInfo(DatastoreVendor.Memcached.ToKnownName(), null, -1, null);
}

try
{
var targetType = target.GetType();
_transformerGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(targetType, "KeyTransformer");
var transformer = _transformerGetter(target);

_transformMethod ??= VisibilityBypasser.Instance.GenerateOneParameterMethodCaller<string, string>(AssemblyName, transformer.GetType().FullName, "Transform");
var hashedKey = _transformMethod(transformer, key);

_poolGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(targetType, "Pool");
var pool = _poolGetter(target);

_locateMethod ??= VisibilityBypasser.Instance.GenerateOneParameterMethodCaller<string, object>(AssemblyName, pool.GetType().FullName, "Enyim.Caching.Memcached.IServerPool.Locate");
var node = _locateMethod(pool, hashedKey);

_endpointGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(node.GetType(), "EndPoint");
var endpoint = _endpointGetter(node);

var endpointType = endpoint.GetType();
if (endpointType.Name == "DnsEndPoint") // v2.X
{
_addressGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(endpointType, "Host");
}
else if (endpointType.Name == "IPEndPoint") // v3.X
{
_addressGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(endpointType, "Address");
}
else
{
throw new Exception("EndPoint type, "+ endpointType.Name + ", did not match supported types.");
}

var address = _addressGetter(endpoint).ToString();

_portGetter ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<int>(endpointType, "Port");
int? port = _portGetter(endpoint);

return new ConnectionInfo(DatastoreVendor.Memcached.ToKnownName(), address, port.HasValue ? port.Value : -1, null);
}
catch (Exception exception)
{
agent.Logger.Warn(exception, "Unable to get Memcached server address/port, likely to due to type differences. Server address/port will not be available.");
_hasGetServerFailed = true;
return new ConnectionInfo(DatastoreVendor.Memcached.ToKnownName(), null, -1, null);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

namespace MemcachedTestApp
{
public class BlogPost
{
public string Title { get; set; }
public string Body { get; set; }
}
}
Loading

0 comments on commit 52bdc11

Please sign in to comment.