Skip to content

Commit

Permalink
Add Lettuce instrumentation support for versions 4.4.0.Final to latest
Browse files Browse the repository at this point in the history
  • Loading branch information
lovesh-ap committed Oct 30, 2023
1 parent 53a01b7 commit c5d4791
Show file tree
Hide file tree
Showing 18 changed files with 736 additions and 1 deletion.
22 changes: 22 additions & 0 deletions instrumentation-security/lettuce-4.3/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
dependencies {
implementation(project(":newrelic-security-api"))
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
implementation 'biz.paluch.redis:lettuce:4.4.0.Final'
}

jar {
manifest {
attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.lettuce-4.3'
}
}

verifyInstrumentation {
passesOnly 'biz.paluch.redis:lettuce:[4.4.0.Final,4.5.0.Final]'
excludeRegex '.*SNAPSHOT'
}

site {
title 'Lettuce 4.3'
type 'Framework'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
*
* * Copyright 2022 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/
package com.lambdaworks.redis;

import com.lambdaworks.redis.api.StatefulConnection;
import com.lambdaworks.redis.protocol.*;
import com.newrelic.agent.security.instrumentation.lettuce_4_3.LettuceUtils;
import com.newrelic.api.agent.Trace;
import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
import com.newrelic.api.agent.security.schema.AbstractOperation;
import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException;
import com.newrelic.api.agent.security.schema.operation.RedisOperation;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

import java.util.ArrayList;
import java.util.List;

@Weave(originalName = "com.lambdaworks.redis.AbstractRedisAsyncCommands")
public abstract class AbstractRedisAsyncCommands_Instrumentation<K, V> {

public abstract StatefulConnection<K, V> getConnection();

@SuppressWarnings("unchecked")
@Trace
public <T> AsyncCommand<K, V, T> dispatch(RedisCommand_Instrumentation<K, V, T> cmd) {
boolean isLockAcquired = acquireLockIfPossible();
AbstractOperation operation = null;
if(isLockAcquired) {
operation = preprocessSecurityHook(cmd, LettuceUtils.METHOD_DISPATCH);
}

AsyncCommand<K, V, T> returnVal = null;
try {
returnVal = Weaver.callOriginal();
} finally {
if(isLockAcquired){
releaseLock();
}
}
registerExitOperation(isLockAcquired, operation);

return returnVal;
}

private void registerExitOperation(boolean isProcessingAllowed, com.newrelic.api.agent.security.schema.AbstractOperation operation) {
try {
if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() ||
NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || GenericHelper.skipExistsEvent()
) {
return;
}
NewRelicSecurity.getAgent().registerExitEvent(operation);
} catch (Throwable ignored){}
}

private <T> AbstractOperation preprocessSecurityHook(RedisCommand_Instrumentation<K,V,T> cmd, String methodDispatch) {
try {
if (!NewRelicSecurity.isHookProcessingActive() ||
NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()){
return null;
}
String type = cmd.getType().name();
CommandArgs_Instrumentation commandArgs = cmd.getArgs();
List<Object> arguments = new ArrayList<>();
for(int i=0 ; i<commandArgs.count(); i++){
Object arg = CommandArgsCsecUtils.getSingularArgs(commandArgs).get(i);
arguments.add(CommandArgsCsecUtils.getArgument(arg));
}
System.out.println(String.format("redis command : %s with arguments : %s", type, arguments));
} catch (Throwable e) {
if (e instanceof NewRelicSecurityException) {
throw e;
}
}
return null;
}

private void releaseLock() {
try {
GenericHelper.releaseLock(LettuceUtils.NR_SEC_CUSTOM_ATTRIB_NAME);
} catch (Throwable ignored) {}
}

private boolean acquireLockIfPossible() {
try {
return GenericHelper.acquireLockIfPossible(LettuceUtils.NR_SEC_CUSTOM_ATTRIB_NAME);
} catch (Throwable ignored) {}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.lambdaworks.redis.protocol;

import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

@Weave(type = MatchType.ExactClass, originalName = "com.lambdaworks.redis.protocol.CommandArgs$BytesArgument")
abstract class BytesArgument_Instrumentation {
private final byte[] val = Weaver.callOriginal();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.lambdaworks.redis.protocol;

import java.util.List;

public class CommandArgsCsecUtils {

public static List<CommandArgs.SingularArgument> getSingularArgs(CommandArgs_Instrumentation commandArgs) {
return commandArgs.singularArguments;
}

public static byte[] getByteArgumentVal(CommandArgs.BytesArgument bytesArgument) {
return bytesArgument.val;
}

public static long getIntegerArgument(CommandArgs.IntegerArgument integerArgument) {
return integerArgument.val;
}

public static double getDoubleArgument(CommandArgs.DoubleArgument doubleArgument) {
return doubleArgument.val;
}

public static String getStringArgument(CommandArgs.StringArgument stringArgument) {
return stringArgument.val;
}

public static char[] getCharArrayArgument(CommandArgs.CharArrayArgument charArrayArgument) {
return charArrayArgument.val;
}

public static Object getKeyArgument(CommandArgs.KeyArgument keyArgument) {
return keyArgument.key;
}

public static Object getValueArgument(CommandArgs.ValueArgument valueArgument) {
return valueArgument.val;
}

public static Object getArgument(Object arg) {
if (arg instanceof CommandArgs.BytesArgument){
return getByteArgumentVal((CommandArgs.BytesArgument) arg);
} else if (arg instanceof CommandArgs.IntegerArgument) {
return getIntegerArgument((CommandArgs.IntegerArgument) arg);
} else if (arg instanceof CommandArgs.DoubleArgument) {
return getDoubleArgument((CommandArgs.DoubleArgument) arg);
} else if (arg instanceof CommandArgs.StringArgument) {
return getStringArgument((CommandArgs.StringArgument) arg);
} else if (arg instanceof CommandArgs.CharArrayArgument) {
return getCharArrayArgument((CommandArgs.CharArrayArgument) arg);
} else if (arg instanceof CommandArgs.KeyArgument) {
return getKeyArgument((CommandArgs.KeyArgument) arg);
} else if (arg instanceof CommandArgs.ValueArgument) {
return getValueArgument((CommandArgs.ValueArgument) arg);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.lambdaworks.redis.protocol;

import com.newrelic.api.agent.weaver.NewField;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

import java.util.ArrayList;
import java.util.List;

@Weave(originalName = "com.lambdaworks.redis.protocol.CommandArgs")
public class CommandArgs_Instrumentation<K, V> {

final List<CommandArgs.SingularArgument> singularArguments = Weaver.callOriginal();

// @NewField
// public List<CommandArgs.SingularArgument> singularArguments_instrumentation;

public int count() {
return Weaver.callOriginal();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.lambdaworks.redis.protocol;

import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;

@Weave(type = MatchType.Interface, originalName = "com.lambdaworks.redis.protocol.RedisCommand")
public abstract class RedisCommand_Instrumentation<K, V, T> {

public abstract ProtocolKeyword getType();
public CommandArgs_Instrumentation<K, V> getArgs() {
return Weaver.callOriginal();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.newrelic.agent.security.instrumentation.lettuce_4_3;

public class LettuceUtils {

public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "REDIS_OPERATION_LOCK_LETTUCE-";

public static final String NR_SEC_CUSTOM_ATTR_FILTER_NAME = "REDIS_FILTER-";
public static final String METHOD_DISPATCH = "dispatch";

public static String getNrSecCustomAttribName(int hashCode) {
return NR_SEC_CUSTOM_ATTR_FILTER_NAME + hashCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
*
* * Copyright 2022 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/
package com.nr.lettuce43.instrumentation;

import com.newrelic.agent.bridge.datastore.DatastoreVendor;
import com.newrelic.agent.introspec.DatastoreHelper;
import com.newrelic.agent.introspec.InstrumentationTestConfig;
import com.newrelic.agent.introspec.InstrumentationTestRunner;
import com.newrelic.agent.introspec.Introspector;
import com.nr.lettuce43.instrumentation.helper.Data;
import com.nr.lettuce43.instrumentation.helper.RedisDataService;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;

import java.util.Collection;
import java.util.concurrent.ExecutionException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

@RunWith(InstrumentationTestRunner.class)
@InstrumentationTestConfig(includePrefixes = {"com.lambdaworks.redis"})
public class Lettuce43InstrumentationTest {

@Rule
public GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:5.0.3-alpine"))
.withExposedPorts(6379);
private RedisDataService redisDataService;

@Before
public void before() {
redisDataService = new RedisDataService(redis);
redisDataService.init();
}

@Test
public void testSync() {
// given some data
String key = "syncKey";
String value = "syncValue";

// when sync 'set' called
String response = redisDataService.syncSet(key, value);

// then response should be key
assertEquals("Then response should be key", key, response);

// when 'get' called
String received = redisDataService.syncGet(key);

// then value returned
assertEquals("Get value", value, received);

// and 2 transactions have been sent
Introspector introspector = InstrumentationTestRunner.getIntrospector();
assertEquals("Finished transaction count", 2, introspector.getFinishedTransactionCount(1000));
Collection<String> transactionNames = introspector.getTransactionNames();
assertEquals("Transaction name count", 2, transactionNames.size());

String setTransactionName = "OtherTransaction/Custom/com.nr.lettuce43.instrumentation.helper.RedisDataService/syncSet";
String getTransactionName = "OtherTransaction/Custom/com.nr.lettuce43.instrumentation.helper.RedisDataService/syncGet";

// and transaction names are in collection
assertTrue("Should contain transaction name for 'set'", transactionNames.contains(setTransactionName));
assertTrue("Should contain transaction name for 'get'", transactionNames.contains(getTransactionName));

// and required datastore metrics are sent
DatastoreHelper helper = new DatastoreHelper(DatastoreVendor.Redis.name());
helper.assertAggregateMetrics();

assertEquals(1, introspector.getTransactionEvents(setTransactionName).iterator().next().getDatabaseCallCount());
assertEquals(1, introspector.getTransactionEvents(getTransactionName).iterator().next().getDatabaseCallCount());
helper.assertUnscopedOperationMetricCount("SET", 1);
helper.assertUnscopedOperationMetricCount("GET", 1);

helper.assertInstanceLevelMetric(DatastoreVendor.Redis.name(), redis.getHost(), redis.getFirstMappedPort().toString());
}

@Test
public void testAsync() throws ExecutionException, InterruptedException {
// given some data
Data data = new Data("asyncKey1", "asyncValue1");

// when async 'set' called
String response = redisDataService.asyncSet(data);

// then response should be 'OK'
assertEquals("Then response should be 'OK'", "OK", response);

// when async 'get' called
String value = redisDataService.asyncGet(data.key);

// then value returned
assertEquals("Get value", data.value, value);

// and 2 transactions have been sent
Introspector introspector = InstrumentationTestRunner.getIntrospector();
assertEquals("Finished transaction count", 2, introspector.getFinishedTransactionCount(1000));
Collection<String> transactionNames = introspector.getTransactionNames();
assertEquals("Transaction name count", 2, transactionNames.size());

String setTransactionName = "OtherTransaction/Custom/com.nr.lettuce43.instrumentation.helper.RedisDataService/asyncSet";
String getTransactionName = "OtherTransaction/Custom/com.nr.lettuce43.instrumentation.helper.RedisDataService/asyncGet";

// and transaction names are in collection
assertTrue("Should contain transaction name for 'set'", transactionNames.contains(setTransactionName));
assertTrue("Should contain transaction name for 'get'", transactionNames.contains(getTransactionName));

// and required datastore metrics are sent
DatastoreHelper helper = new DatastoreHelper("Redis");
helper.assertAggregateMetrics();
assertEquals(1, introspector.getTransactionEvents(setTransactionName).iterator().next().getDatabaseCallCount());
assertEquals(1, introspector.getTransactionEvents(getTransactionName).iterator().next().getDatabaseCallCount());
helper.assertUnscopedOperationMetricCount("SET", 1);
helper.assertUnscopedOperationMetricCount("GET", 1);
helper.assertInstanceLevelMetric(DatastoreVendor.Redis.name(), redis.getHost(), redis.getFirstMappedPort().toString());
}
}
Loading

0 comments on commit c5d4791

Please sign in to comment.