package org.ff4j.store; /* * #%L * ff4j-store-redis * %% * Copyright (C) 2013 - 2016 FF4J * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.ff4j.audit.Event; import org.ff4j.audit.EventConstants; import org.ff4j.audit.EventQueryDefinition; import org.ff4j.audit.EventSeries; import org.ff4j.audit.MutableHitCount; import org.ff4j.audit.chart.TimeSeriesChart; import org.ff4j.audit.repository.AbstractEventRepository; import org.ff4j.redis.RedisConnection; import org.ff4j.redis.RedisContants; import org.ff4j.utils.Util; import redis.clients.jedis.Jedis; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import static org.ff4j.redis.RedisContants.KEY_EVENT; /** * Persist audit events into REDIS storage technology. * * @author clunven * @author Shridhar Navanageri */ public class EventRepositoryRedis extends AbstractEventRepository { public static final int UPPER_LIMIT = 50000; /** * Wrapping of redis connection (isolation). */ private RedisConnection redisConnection; /** * Jackson ObjectMapper for serialization and deserialization purpose. */ private static ObjectMapper objectMapper = new ObjectMapper(); static { // Avoiding accidental breaks, since Redis is a schema-free store. objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } /** Enumeration containing the type of the attribute. */ private enum Types { SOURCE, NAME, HOST, USER; } /** * Patternt to create KEY. */ private static final SimpleDateFormat SDF_KEY = new SimpleDateFormat("yyyyMMdd"); /** * Default Constructor. */ public EventRepositoryRedis() { this(new RedisConnection()); } /** * Contact remote redis server. * * @param pRedisConnection Redis connection instance. */ public EventRepositoryRedis(RedisConnection pRedisConnection) { redisConnection = pRedisConnection; } /** * Contact remote redis server. * * @param host target redis host * @param port target redis port */ public EventRepositoryRedis(String host, int port) { this(new RedisConnection(host, port)); } /** * Contact remote redis server. * * @param host target redis host * @param port target redis port * @param password the password for connecting to Redis if auth enabled. */ public EventRepositoryRedis(String host, int port, String password) { this(new RedisConnection(host, port, password)); } /** {@inheritDoc} */ @Override public void createSchema() { // Keys are automatically generated and created } /** {@inheritDoc} */ public boolean saveEvent(Event evt) { if (evt == null) { throw new IllegalArgumentException("Event cannot be null nor empty"); } Jedis jedis = null; try { jedis = getJedis(); long timeStamp = evt.getTimestamp(); String hashId = this.getHashKey(evt.getTimestamp(), evt.getAction()); evt.setUuid(String.valueOf(timeStamp)); jedis.zadd(hashId, timeStamp, objectMapper.writeValueAsString(evt)); return true; } catch (JsonProcessingException e) { // We do not returned false, it will be retried 3 times for nothing, faile immediately throw new IllegalArgumentException("Cannot save event : invalid object", e); } finally { if (jedis != null) { jedis.close(); } } } private String getHashKey(long timestamp, String action) { String hashId = KEY_EVENT; if (action != null) { hashId += RedisContants.KEY_EVENT_AUDIT + "_"; } long timeStamp = timestamp; hashId += SDF_KEY.format(new Date(timeStamp)); return hashId; } /** {@inheritDoc} */ @Override public Event getEventByUUID(String uuid, Long timestamp) { Util.assertHasLength(new String[]{uuid}); Event redisEvent = null; Jedis jedis = null; try { jedis = getJedis(); String hashKey = getHashKey(timestamp, null); // Check for the event within 100ms time range passed, hoping there won't be more than 10 for this. Set<String> events = jedis.zrangeByScore(hashKey, timestamp - 100L, timestamp + 100L, 0, 10); // Loop through the result set and match the timestamp passed in. for (String evt : events) { Event event = marshallEvent(evt); if (timestamp == event.getTimestamp()) { return event; } } } finally { if (jedis != null) { jedis.close(); } } return redisEvent; } private Event marshallEvent(String eventString) { try { return objectMapper.readValue(eventString, Event.class); } catch (JsonParseException e) { throw new IllegalArgumentException("Cannot read event from DB, cannot parse", e); } catch (JsonMappingException e) { throw new IllegalArgumentException("Cannot read event from DB, cannot map", e); } catch (IOException e) { throw new IllegalArgumentException("Cannot read event from DB", e); } } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getFeatureUsageHitCount(EventQueryDefinition query) { return getUsageCount(query, Types.NAME); } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getHostHitCount(EventQueryDefinition query) { return getUsageCount(query, Types.HOST); } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getUserHitCount(EventQueryDefinition query) { return getUsageCount(query, Types.USER); } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getSourceHitCount(EventQueryDefinition query) { return getUsageCount(query, Types.SOURCE); } private Map<String, MutableHitCount> getUsageCount(EventQueryDefinition query, Types type) { Map<String, MutableHitCount> hitCount = new HashMap<>(); // Get events from Redis between the time range. Set<String> events = getEventsFromRedis(query); // Loop through create the buckets. for (String event : events) { Event eventObject = marshallEvent(event); String value = getValueFromAttribute(type, eventObject); MutableHitCount mutableHitCount = hitCount.get(value); if (mutableHitCount != null) { mutableHitCount.inc(); } else { mutableHitCount = new MutableHitCount(1); } hitCount.put(value, mutableHitCount); } return hitCount; } /** {@inheritDoc} */ @Override public TimeSeriesChart getFeatureUsageHistory(EventQueryDefinition query, TimeUnit tu) { TimeSeriesChart tsc = new TimeSeriesChart(query.getFrom(), query.getTo(), tu); Set<String> events = getEventsFromRedis(query); for (String event : events) { tsc.addEvent(marshallEvent(event)); } return tsc; } /** {@inheritDoc} */ @Override public EventSeries searchFeatureUsageEvents(EventQueryDefinition query) { // Referenced from RDBMS implementation, same usage. return getAuditTrail(query); } /** {@inheritDoc} */ @Override public EventSeries getAuditTrail(EventQueryDefinition query) { Jedis jedis = null; EventSeries eventSeries = new EventSeries(); try { jedis = getJedis(); String hashKey = getHashKey(query.getFrom(), EventConstants.ACTION_CHECK_OK); Set<String> events = jedis.zrangeByScore(hashKey, query.getFrom(), query.getTo(), 0, 100); // FIXME: Server side pagination model isn't present? This could be a lot of data. for (String event : events) { eventSeries.add(marshallEvent(event)); } } finally { if (jedis != null) { jedis.close(); } } return eventSeries; } /** {@inheritDoc} */ @Override public void purgeAuditTrail(EventQueryDefinition query) { // TBD: Where is the setting to turn purging on/off and what would be the time range? } /** {@inheritDoc} */ @Override public void purgeFeatureUsage(EventQueryDefinition query) { // TBD: Where is the setting to turn purging on/off and what would be the time range? } /** * Safe acces to Jedis, avoid JNPE. * * @return access jedis */ public Jedis getJedis() { if (redisConnection == null) { throw new IllegalArgumentException("Cannot found any redisConnection"); } Jedis jedis = redisConnection.getJedis(); if (jedis == null) { throw new IllegalArgumentException("Cannot found any jedis connection, please build connection"); } return jedis; } /** * Method that reads the raw event stream from Redis. * * @param query - The query object containing details about the query. * @return Set containing raw events. */ private Set<String> getEventsFromRedis(EventQueryDefinition query) { Jedis jedis = null; Set<String> events = null; try { jedis = getJedis(); String hashKey = getHashKey(query.getFrom(), null); events = jedis.zrangeByScore(hashKey, query.getFrom(), query.getTo(), 0, UPPER_LIMIT); } finally { if (jedis != null) { jedis.close(); } } return events; } /** * Method that maps the enum to the appropriate event method (instead of using Reflection). * * @param type - The type of the evnt to be used. * @param event - Actual event object. * @return The value from the event object. */ private String getValueFromAttribute(Types type, Event event) { String value; switch (type) { case HOST: value = event.getHostName(); break; case SOURCE: value = event.getSource(); break; case USER: value = event.getUser(); break; case NAME: value = event.getName(); break; default: value = "NA"; } return value; } }