14 Commits

Author SHA1 Message Date
97ac9b0c4b Update redis summary page 2024-03-02 09:31:34 +01:00
a219bc672b Update redis summary page 2024-03-02 09:22:09 +01:00
43b21ad0a9 Ignore target dir 2024-03-02 09:20:28 +01:00
c597467656 v1.0.3: Support string list, with TTL 2024-02-04 16:51:04 +01:00
4b032a90cf Fix metrics 2024-02-04 13:07:19 +01:00
e1f6e0ad60 v1.0.2 : Handle no key expiration with TTL = -1 2024-02-04 11:37:59 +01:00
e77d4d915d typo 2024-02-04 11:35:56 +01:00
0a0748172e Handle TTL to -1 = no key expiration 2024-02-04 11:33:31 +01:00
8febd14eb6 Comment on redis_lookup function: do not use 2024-02-04 11:33:07 +01:00
2cf5dff011 v1.0.1: Handle TTL and clear key 2024-02-04 10:42:12 +01:00
30e1bf70d9 Minor fixes so this compile 2024-02-04 10:40:25 +01:00
yo
f1c53077a4 Handle disconnect at doStop 2024-02-03 22:52:46 +01:00
yo
5a4b3cb38a Add clearKey and assignTtl 2024-02-03 22:38:36 +01:00
yo
5487e2dd71 Support TTL setting in 'lookup_set_value(lookup_table, key, value, ttl)' 2024-02-03 22:19:50 +01:00
8 changed files with 269 additions and 26 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules/*
target/*
/target/

View File

@ -1,6 +1,6 @@
# RedisLookupPlugin Plugin for Graylog
Plugin to add Redis Data Adapter in read/write to graylog so you can store and retrieve key/values from pipelines
Plugin to add Redis Data Adapter in read/write to graylog so you can store and retrieve key/values and lists of string values from pipelines
Support Redis authentication (with password and username/password)
**Required Graylog version:** 5.0 and later
@ -8,7 +8,7 @@ Support Redis authentication (with password and username/password)
Installation
------------
[Download the plugin](https://git.nosd.in/yo/graylog-redis-lookup-plugin/releases/download/v1.0.0/graylog-plugin-redis-lookup-1.0.0.jar)
[Download the plugin](https://git.nosd.in/yo/graylog-redis-lookup-plugin/releases/download/v1.0.3/graylog-plugin-redis-lookup-1.0.3.jar)
and place the `.jar` file in your Graylog plugin directory. The plugin directory
is the `plugins/` folder relative from your `graylog-server` directory by default
and can be configured in your `graylog.conf` file.
@ -30,8 +30,24 @@ Usage
-----
* Create data adapter, cache (or not), lookup table
* Use 'lookup_set_value(lookup_table, key, value)' to create or update key in redis
* Use 'lookup(lookup_table, key)' to get key
* Use 'lookup_set_value(lookup_table, key, value, [ttl])' to create or update key in redis
* Use 'lookup_value(lookup_table, key)' to get key value
* Use 'lookup_clear_key(lookup_table, key)' to remove key
* Use 'lookup_has_value(lookup_table, key)' to test key existence
* Use 'lookup_assign_ttl(lookup_table, key, ttl)' to change TTL of existing key
* Use 'lookup_set_string_list(lookup_table, key, value, [ttl])' to create a list named "key"
* Use 'lookup_add_string_list(lookup_table, key, value, [keep_duplicates])' to add value list to existing list
* Use 'lookup_remove_string_list(lookup_table, key, value) to remove a string from list "key"
By default single value keys will be created in Redis with the default TTL defined at data adapter creation time
Be aware that only setting TTL with 'lookup_assign_ttl' of 'lookup_set_string_list' alter TTL value in Redis ; so a list created with a TTL of 3600 will expire in 3600 seconds, even if it was updated with 'lookup_add_string_list' some seconds before expiration.
Known bugs
----------
Deletion via lookup_remove_string_list, lookup_clear_key or keep_duplicates=false sometimes not done.
Getting started
---------------

View File

@ -10,7 +10,7 @@
<groupId>in.nosd.redis</groupId>
<artifactId>graylog-plugin-redis-lookup</artifactId>
<name>${project.artifactId}</name>
<version>1.0.0</version>
<version>1.0.3</version>
<description>Graylog ${project.artifactId} plugin.</description>
<url>https://www.graylog.org</url>
<developers>

View File

@ -31,7 +31,7 @@
<groupId>in.nosd.redis</groupId>
<artifactId>graylog-plugin-redis-lookup</artifactId>
<version>1.0.0</version>
<version>1.0.3</version>
<packaging>jar</packaging>
<name>${project.artifactId}</name>

View File

@ -68,11 +68,23 @@ public class RedisLookupDataAdapter extends LookupDataAdapter {
private final Config config;
private final RedisClient client;
private RedisCommands<String, String> commands;
private StatefulRedisConnection<String, String> connection;
private final Timer redisGetRequestTimer;
private final Meter redisGetRequestErrors;
private final Timer redisSetRequestTimer;
private final Meter redisSetRequestErrors;
private final Timer redisDelRequestTimer;
private final Meter redisDelRequestErrors;
private final Timer redisAssignTtlRequestTimer;
private final Meter redisAssignTtlRequestErrors;
private final Timer redisAddStringListRequestTimer;
private final Meter redisAddStringListRequestErrors;
private final Timer redisSetStringListRequestTimer;
private final Meter redisSetStringListRequestErrors;
private final Timer redisSetStringListWithTtlRequestTimer;
private final Meter redisSetStringListWithTtlRequestErrors;
private final Timer redisRemoveStringListRequestTimer;
private final Meter redisRemoveStringListRequestErrors;
@Inject
public RedisLookupDataAdapter(@Assisted("dto") DataAdapterDto dto,
@ -97,21 +109,33 @@ public class RedisLookupDataAdapter extends LookupDataAdapter {
this.redisGetRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisGetRequestErrors"));
this.redisSetRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "redisSetRequestTime"));
this.redisSetRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisSetRequestErrors"));
this.redisDelRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "redisDelRequestTime"));
this.redisDelRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisDelRequestErrors"));
this.redisAssignTtlRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "redisAssignTtlRequestTime"));
this.redisAssignTtlRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisAssignTtlRequestErrors"));
this.redisAddStringListRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "redisAddStringListRequestTime"));
this.redisAddStringListRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisAddStringListRequestErrors"));
this.redisSetStringListRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "redisSetStringListRequestTime"));
this.redisSetStringListRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisSetStringListRequestErrors"));
this.redisSetStringListWithTtlRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "redisSetStringListWithTtlRequestTime"));
this.redisSetStringListWithTtlRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisSetStringListWithTtlRequestErrors"));
this.redisRemoveStringListRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "redisRemoveStringListRequestTime"));
this.redisRemoveStringListRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "redisRemoveStringListRequestErrors"));
}
// Add code to initialise Redis connection
@Override
protected void doStart() throws Exception {
StatefulRedisConnection<String, String> connection = this.client.connect();
connection = this.client.connect();
this.commands = connection.sync();
}
// Add code to close Redis connection
@Override
protected void doStop() throws Exception {
connection.close();
client.close();
}
// Returns the refresh interval for this data adapter. Use {@link Duration#ZERO} if refresh should be disabled.
@Override
public Duration refreshInterval() {
return REFRESH_INTERVAL_DURATION;
@ -123,6 +147,11 @@ public class RedisLookupDataAdapter extends LookupDataAdapter {
cachePurge.purgeAll();
}
private String getSingleValue(String key) {
final String value = this.commands.get(key);
return value;
}
@Override
protected LookupResult doGet(Object key) {
final Timer.Context time = redisGetRequestTimer.time();
@ -132,15 +161,22 @@ public class RedisLookupDataAdapter extends LookupDataAdapter {
return getEmptyResult();
}
try {
final String value = this.commands.get(trimmedKey);
if (value == null) {
LOG.warn("Redis GET request for key <{}> returned null, key do not exists.", trimmedKey);
redisGetRequestErrors.mark();
return LookupResult.empty();
// Get item type and existence
final String type = this.commands.type(trimmedKey);
switch(type) {
case "none":
LOG.debug("Redis TYPE request for key <{}> returned null, key do not exists.", trimmedKey);
redisGetRequestErrors.mark();
return LookupResult.empty();
case "list":
final List<String> result = this.commands.lrange(trimmedKey, 0, -1);
return LookupResult.withoutTTL().stringListValue(result).build();
default:
final String value = getSingleValue(trimmedKey);
return LookupResult.single(value);
}
return LookupResult.single(value);
} catch (Exception e) {
LOG.error("Redis GET request error for key <{}>", trimmedKey, e);
LOG.error("Exception: Redis GET request error for key <{}>", trimmedKey, e);
redisGetRequestErrors.mark();
return LookupResult.empty();
} finally {
@ -149,25 +185,205 @@ public class RedisLookupDataAdapter extends LookupDataAdapter {
}
// This is deprecated, see setValue
@Override
@Deprecated
public void set(Object key, Object value) {
return;
}
@Override
public LookupResult setValue(Object key, Object value) {
return setValueWithTtl(key, value, this.config.redisKeyTTL());
}
public LookupResult setValueWithTtl(Object key, Object value, Long ttlSec) {
final Timer.Context time = redisSetRequestTimer.time();
final String trimmedKey = StringUtils.trimToNull(key.toString());
try {
final String result = this.commands.setex(key.toString(), this.config.redisKeyTTL(), value.toString());
final String result;
if (ttlSec > 0 ) {
result = this.commands.setex(trimmedKey, ttlSec, value.toString());
} else {
result = this.commands.set(trimmedKey, value.toString());
}
if (!result.equals("OK")) {
LOG.warn("Redis SET key <{}> to value <{}> returned {}", key, value, result);
LOG.warn("Redis SET(EX) key <{}> to value <{}> with TTL <{}> returned {}", key, value, ttlSec, result);
redisSetRequestErrors.mark();
return LookupResult.empty();
}
return LookupResult.single(value.toString());
} catch (Exception e) {
LOG.error("Redis SET key <{}> to value <{}> returned an exception: {}", key, value, e);
LOG.error("Exception: Redis SET(EX) key <{}> to value <{}> with TTL <{}> returned {}", key, value, ttlSec, e);
redisSetRequestErrors.mark();
return LookupResult.withError();
} finally {
time.stop();
}
}
@Override
public void clearKey(Object key) {
final Timer.Context time = redisDelRequestTimer.time();
final String trimmedKey = StringUtils.trimToNull(key.toString());
try {
final Long result = this.commands.del(trimmedKey);
if (result != 1) {
LOG.debug("Redis DEL key <{}> returned {}", trimmedKey, result);
redisDelRequestErrors.mark();
}
return;
} catch (Exception e) {
LOG.error("Exception: Redis DEL key <{}> returned {}", trimmedKey, e);
redisDelRequestErrors.mark();
return;
} finally {
time.stop();
}
}
private LookupResult setExpire(String key, Long ttl) {
try {
if (!this.commands.expire(key, ttl)) {
LOG.warn("Redis EXPIRE key <{}> to <{}> returned false (key does not exist or the timeout could not be set)", key, ttl);
return LookupResult.withError();
}
return LookupResult.single(this.commands.get(key).toString());
}
catch (Exception e) {
// lettuce 6.3.0 returns "WRONGTYPE Operation against a key holding the wrong kind of value" when EXPIRE on a list key, but do the job.
if (e.getMessage().startsWith("WRONGTYPE Operation against a key holding the wrong kind of value")) {
} else {
LOG.error("Exception: Redis EXPIRE key <{}> to <{}> returned {}", key, ttl, e);
return LookupResult.withError(e.toString());
}
}
return LookupResult.single(this.commands.get(key).toString());
}
private LookupResult setPersist(String key) {
try {
if (!this.commands.persist(key)) {
LOG.debug("Redis PERSIST key <{}> returned false (key does not exist or does not have an associated timeout)", key);
return LookupResult.withError();
}
return LookupResult.single(this.commands.get(key).toString());
} catch (Exception e) {
LOG.error("Exception: Redis PERSIST key <{}> returned {}", key, e);
return LookupResult.withError(e.toString());
}
}
// TTL -1 = never expire
public LookupResult assignTtl(Object key, Long ttlSec) {
final Timer.Context time = redisAssignTtlRequestTimer.time();
final String trimmedKey = StringUtils.trimToNull(key.toString());
try {
if (ttlSec > 0) {
return setExpire(trimmedKey, ttlSec);
} else {
return setPersist(trimmedKey);
}
} catch (Exception e) {
// lettuce 6.3.0 returns "WRONGTYPE Operation against a key holding the wrong kind of value" when TTL on a list key, but do the job.
if (e.getMessage().startsWith("WRONGTYPE Operation against a key holding the wrong kind of value")) {
} else {
LOG.error("Exception: assignTtl <{}> to key <{}> returned {}", ttlSec, trimmedKey, e);
redisAssignTtlRequestErrors.mark();
return LookupResult.withError();
}
} finally {
time.stop();
}
return LookupResult.single(this.commands.get(trimmedKey));
}
@Override
public LookupResult addStringList(Object key, List<String> listValue, boolean keepDuplicates) {
final Timer.Context time = redisAddStringListRequestTimer.time();
final String trimmedKey = StringUtils.trimToNull(key.toString());
if (trimmedKey == null) {
LOG.debug("A blank key was supplied");
return getEmptyResult();
}
try {
if (!keepDuplicates) {
removeStringList(trimmedKey, listValue);
}
final Long len = this.commands.rpush(trimmedKey, listValue.toArray(new String[0]));
if (len > 0) {
return LookupResult.withoutTTL().stringListValue(this.commands.lrange(trimmedKey, 0, -1)).build();
}
return LookupResult.empty();
} catch (Exception e) {
LOG.error("Exception: Redis RPUSH request error for key <{}>: <{}>", trimmedKey, e);
redisAddStringListRequestErrors.mark();
return LookupResult.empty();
} finally {
time.stop();
}
}
@Override
public LookupResult setStringList(Object key, List<String> listValue) {
final Timer.Context time = redisSetStringListRequestTimer.time();
final String trimmedKey = StringUtils.trimToNull(key.toString());
if (trimmedKey == null) {
LOG.debug("A blank key was supplied");
return getEmptyResult();
}
try {
// We need to replace list, so we delete it first
this.commands.ltrim(trimmedKey, 1, 0);
final Long len = this.commands.rpush(trimmedKey, listValue.toArray(new String[0]));
if (len > 0) {
return LookupResult.withoutTTL().stringListValue(this.commands.lrange(trimmedKey, 0, -1)).build();
}
return LookupResult.empty();
} catch (Exception e) {
LOG.error("Exception: Redis RPUSH request error for key <{}>: <{}>", trimmedKey, e);
redisSetStringListRequestErrors.mark();
return LookupResult.empty();
} finally {
time.stop();
}
}
public LookupResult setStringListWithTtl(Object key, List<String> listValue, Long ttlSec) {
final Timer.Context time = redisSetStringListWithTtlRequestTimer.time();
try {
setStringList(key, listValue);
return(assignTtl(key, ttlSec));
} catch (Exception e) {
// This exception comes from assignTtl
if (e.getMessage().startsWith("WRONGTYPE Operation against a key holding the wrong kind of value")) {
} else {
LOG.error("Exception: Redis RPUSH request error for key <{}>: <{}>", key, e);
redisSetStringListWithTtlRequestErrors.mark();
return LookupResult.empty();
}
} finally {
time.stop();
}
return LookupResult.withoutTTL().stringListValue(this.commands.lrange(StringUtils.trimToNull(key.toString()), 0, -1)).build();
}
@Override
public LookupResult removeStringList(Object key, List<String> listValue) {
final Timer.Context time = redisRemoveStringListRequestTimer.time();
final String trimmedKey = StringUtils.trimToNull(key.toString());
if (trimmedKey == null) {
LOG.debug("A blank key was supplied");
return getEmptyResult();
}
try {
long removed = 0;
for (String value : listValue) {
removed += this.commands.lrem(trimmedKey, 0, value);
}
LOG.debug("Redis LREM for key <{}> and value <{}> deleted <{}> items", trimmedKey, listValue, removed);
return LookupResult.withoutTTL().stringListValue(this.commands.lrange(trimmedKey, 0, -1)).build();
} catch (Exception e) {
LOG.error("Exception: Redis LREM request error for key <{}> and value <{}>: <{}>", trimmedKey, listValue, e);
redisRemoveStringListRequestErrors.mark();
return LookupResult.empty();
} finally {
time.stop();
@ -194,7 +410,7 @@ public class RedisLookupDataAdapter extends LookupDataAdapter {
.redisHost("127.0.0.1")
.redisPort(6379)
.redisDB(0)
.redisKeyTTL(86400)
.redisKeyTTL(-1)
.redisUsername("")
.redisPassword("")
.build();

View File

@ -76,7 +76,7 @@ public class RedisLookupPluginFunction extends LookupTableFunction<GenericLookup
public FunctionDescriptor<GenericLookupResult> descriptor() {
return FunctionDescriptor.<GenericLookupResult>builder()
.name(NAME)
.description("Match a key into Redis instance and return value")
.description("Match a key into Redis instance and return value. Do not use, prefer standard 'lookup_*' functions")
.params(keyParam)
.returnType(GenericLookupResult.class)
.build();

View File

@ -82,7 +82,7 @@ class RedisLookupAdapterFieldSet extends React.Component {
label="Redis key TTL"
required
onChange={this.props.handleFormEvent}
help={this.props.validationMessage('redis_ttl', 'Redis key TTL in seconds')}
help={this.props.validationMessage('redis_ttl', 'Redis key TTL in seconds. Set -1 to not expire keys')}
bsStyle={this.props.validationState('redis_ttl')}
value={config.redis_ttl}
labelClassName="col-sm-3"

View File

@ -34,6 +34,16 @@ class RedisLookupAdapterSummary extends React.Component {
<dl>
<dt>Redis host</dt>
<dd>{config.redis_host || 'n/a'}</dd>
<dt>Redis port</dt>
<dd>{config.redis_port || 'n/a'}</dd>
<dt>Redis database</dt>
<dd>{config.redis_database}</dd>
<dt>Redis key TTL</dt>
<dd>{config.redis_ttl || 'n/a'}</dd>
<dt>Redis username</dt>
<dd>{config.redis_username || 'n/a'}</dd>
<dt>Redis password</dt>
<dd>******</dd>
</dl>
);
}