package org.ff4j.audit.repository; /* * #%L * ff4j-core * %% * 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 java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; 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.Serie; import org.ff4j.audit.chart.TimeSeriesChart; import org.ff4j.utils.Util; /** * Implementation of in memory {@link EventRepository} with limited events. * * @author Cedrick Lunven (@clunven) */ public class InMemoryEventRepository extends AbstractEventRepository { /** default retention. */ private static final int DEFAULT_QUEUE_CAPACITY = 100000; /** current capacity. */ private int queueCapacity = DEFAULT_QUEUE_CAPACITY; /** Event <YYYYMMDD> / <featureUID> -> <Event> list (only action CHECK_ON) */ private Map<String, Map<String, EventSeries>> featureUsageEvents = new ConcurrentHashMap<String, Map<String, EventSeries>>(); /** Event <YYYYMMDD> -> <featureUID> -> <Event> list (only action CHECK_OFF) */ private Map<String, Map<String, EventSeries>> checkOffEvents = new ConcurrentHashMap<String, Map<String, EventSeries>>(); /** Event <YYYYMMDD> -> Event related to user action in console (not featureUsage, not check OFF). */ private Map<String, EventSeries> auditTrailEvents = new ConcurrentHashMap<String, EventSeries>(); /** * Default constructor with default capacity to 100.000 */ public InMemoryEventRepository() { this(DEFAULT_QUEUE_CAPACITY); } /** * Constructor to tune capacity. * * @param queueCapacity * default queue capacity */ public InMemoryEventRepository(int queueCapacity) { this.queueCapacity = queueCapacity; } /** {@inheritDoc} */ @Override public void createSchema() { // There is nothing to create for inMemeory store return; } /** {@inheritDoc} */ @Override public boolean saveEvent(Event e) { Util.assertEvent(e); if (EventConstants.ACTION_CHECK_OK.equalsIgnoreCase(e.getAction())) { return saveEvent(e, featureUsageEvents); } else if (EventConstants.ACTION_CHECK_OFF.equalsIgnoreCase(e.getAction())) { return saveEvent(e, checkOffEvents); } String key = getKeyDate(e.getTimestamp()); if (!auditTrailEvents.containsKey(key)) { auditTrailEvents.put(key, new EventSeries(this.queueCapacity)); } return auditTrailEvents.get(key).add(e); } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getFeatureUsageHitCount(EventQueryDefinition query) { Map<String, MutableHitCount> hitRatio = new TreeMap<String, MutableHitCount>(); for (Event event : searchFeatureUsageEvents(query)) { if (!hitRatio.containsKey(event.getName())) { hitRatio.put(event.getName(), new MutableHitCount()); } hitRatio.get(event.getName()).inc(); } return hitRatio; } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getSourceHitCount(EventQueryDefinition query) { Map<String, MutableHitCount> hitRatio = new TreeMap<String, MutableHitCount>(); for (Event event : searchFeatureUsageEvents(query)) { if (!hitRatio.containsKey(event.getSource())) { hitRatio.put(event.getSource(), new MutableHitCount()); } hitRatio.get(event.getSource()).inc(); } return hitRatio; } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getHostHitCount(EventQueryDefinition query) { Map<String, MutableHitCount> hitRatio = new TreeMap<String, MutableHitCount>(); for (Event event : searchFeatureUsageEvents(query)) { if (!hitRatio.containsKey(event.getHostName())) { hitRatio.put(event.getHostName(), new MutableHitCount()); } hitRatio.get(event.getHostName()).inc(); } return hitRatio; } /** {@inheritDoc} */ @Override public Map<String, MutableHitCount> getUserHitCount(EventQueryDefinition query) { Map<String, MutableHitCount> hitRatio = new TreeMap<String, MutableHitCount>(); for (Event event : searchFeatureUsageEvents(query)) { String user = Util.hasLength(event.getUser()) ? event.getUser() : "anonymous"; if (!hitRatio.containsKey(user)) { hitRatio.put(user, new MutableHitCount()); } hitRatio.get(user).inc(); } return hitRatio; } /** * Save event to target (based on ACTION). * * @param e * current event * @param target * target list * @return if the evetn is stored */ private boolean saveEvent(Event e, Map<String, Map<String, EventSeries>> target) { String key = getKeyDate(e.getTimestamp()); String uid = e.getName(); if (!target.containsKey(key)) { target.put(key, new ConcurrentHashMap<String, EventSeries>()); } if (!target.get(key).containsKey(uid)) { target.get(key).put(uid, new EventSeries(this.queueCapacity)); } return target.get(key).get(uid).add(e); } /** {@inheritDoc} */ @Override public TimeSeriesChart getFeatureUsageHistory(EventQueryDefinition query, TimeUnit units) { // Create the interval depending on units TimeSeriesChart tsc = new TimeSeriesChart(query.getFrom(), query.getTo(), units); for (String currentDay : getCandidateDays(query.getFrom(), query.getTo())) { // There are some event this day if (featureUsageEvents.containsKey(currentDay)) { for (Map.Entry<String, EventSeries> entry : featureUsageEvents.get(currentDay).entrySet()) { String currentFeatureName = entry.getKey(); // Filter feature names if required Set < String > filteredFeatures = query.getNamesFilter(); if (filteredFeatures == null || filteredFeatures.isEmpty() || filteredFeatures.contains(currentFeatureName)) { // Loop over events for (Event evt : entry.getValue()) { // Between bounds (keydate not enough) if (isEventInInterval(evt, query.getFrom(), query.getTo())) { // Create new serie if new feature Name if (!tsc.getSeries().containsKey((currentFeatureName))) { tsc.createNewSerie(currentFeatureName); } // Match FeatureName Serie < Map<String , MutableHitCount > > serie = tsc.getSeries().get(currentFeatureName); // Match SlotName String slotName = tsc.getSdf().format(new Date(evt.getTimestamp())); // Should be always 'true' as the tsc.getsdf().format() will get a slotName. if (serie.getValue().containsKey(slotName)) { // Fast Increment serie.getValue().get(slotName).inc(); } } } } } } } // Recolor series List < String > colors = Util.generateHSVGradient("ee1100", "442299", tsc.getSeries().size()); int idxColor = 0; for (Map.Entry<String, Serie<Map<String, MutableHitCount>>> serie : tsc.getSeries().entrySet()) { serie.getValue().setColor(colors.get(idxColor)); idxColor++; } return tsc; } /** {@inheritDoc} */ @Override public EventSeries getAuditTrail(EventQueryDefinition q) { EventSeries resultSeries = new EventSeries(10000); for (String currentDay : getCandidateDays(q.getFrom(), q.getTo())) { if (auditTrailEvents.containsKey(currentDay)) { Iterator<Event> iterEvents = auditTrailEvents.get(currentDay).iterator(); while (iterEvents.hasNext()) { Event evt = iterEvents.next(); if (q.match(evt)) { resultSeries.add(evt); } } } } return resultSeries; } /** {@inheritDoc} */ @Override public void purgeAuditTrail(EventQueryDefinition q) { for (String currentDay : getCandidateDays(q.getFrom(), q.getTo())) { if (auditTrailEvents.containsKey(currentDay)) { Iterator<Event> iterEvents = auditTrailEvents.get(currentDay).iterator(); while (iterEvents.hasNext()) { Event evt = iterEvents.next(); if (q.match(evt)) { auditTrailEvents.get(currentDay).remove(evt); } } if (auditTrailEvents.get(currentDay).isEmpty()) { auditTrailEvents.remove(currentDay); } } } } /** {@inheritDoc} */ @Override public void purgeFeatureUsage(EventQueryDefinition q) { Set<String> candidateDates = getCandidateDays(q.getFrom(), q.getTo()); for (String currentDay : candidateDates) { if (featureUsageEvents.containsKey(currentDay)) { Map<String, EventSeries> currentDayEvents = featureUsageEvents.get(currentDay); for (String currentFeature : currentDayEvents.keySet()) { Iterator<Event> iterEvents = currentDayEvents.get(currentFeature).iterator(); while (iterEvents.hasNext()) { Event evt = iterEvents.next(); if (q.match(evt)) { removeEventIfPresent(currentDayEvents.get(currentFeature), evt); } if (currentDayEvents.get(currentFeature).isEmpty()){ currentDayEvents.remove(currentFeature); } } } // Remove list if empty if (currentDayEvents.isEmpty()) { featureUsageEvents.remove(currentDay); } } } } private void removeEventIfPresent(EventSeries es, Event evt) { Iterator < Event > iterEvt = es.iterator(); while(iterEvt.hasNext()) { Event currentEvent = iterEvt.next(); if (currentEvent.getUuid().equalsIgnoreCase(evt.getUuid())) { es.remove(currentEvent); } } } /** {@inheritDoc} */ @Override public EventSeries searchFeatureUsageEvents(EventQueryDefinition query) { EventSeries es = new EventSeries(1000000); // Dates are the keys of the storage map, compute list of keys and loop over them for (String currentDay : getCandidateDays(query.getFrom(), query.getTo())) { // There are some events with the current date if (featureUsageEvents.containsKey(currentDay)) { Map<String, EventSeries> currentDayEvents = featureUsageEvents.get(currentDay); for (String currentFeature : currentDayEvents.keySet()) { // query can have filters for names, here we limite the number of map to scan if (query.matchName(currentFeature)) { Iterator<Event> iterEvents = currentDayEvents.get(currentFeature).iterator(); while (iterEvents.hasNext()) { Event evt = iterEvents.next(); // use other filter (host, action, timestamp....) if (query.match(evt)) { es.add(evt); } } } } } } return es; } /** {@inheritDoc} */ @Override public Event getEventByUUID(String uuid, Long timestamp) { // Limited Search by key if (timestamp != null) { String targetDate = KDF.format(new Date(timestamp.longValue())); return searchEventById(uuid, targetDate); } else { // Full search Set < String > searchedDate = new HashSet<String>(); for(String currentDate : auditTrailEvents.keySet()) { if (!searchedDate.contains(currentDate)) { Event evt = searchEventById(uuid, currentDate); if (evt != null) { return evt; } } searchedDate.add(currentDate); } for(String currentDate : featureUsageEvents.keySet()) { if (!searchedDate.contains(currentDate)) { Event evt = searchEventById(uuid, currentDate); if (evt != null) { return evt; } } searchedDate.add(currentDate); } for(String currentDate : checkOffEvents.keySet()) { if (!searchedDate.contains(currentDate)) { Event evt = searchEventById(uuid, currentDate); if (evt != null) { return evt; } } searchedDate.add(currentDate); } } return null; } /** * Given a date fetch in all the list to find the Event. * * @param uuid * current event unique identifier * @param targetDate * target date * @return * event if found */ private Event searchEventById(String uuid, String targetDate) { Util.assertNotNull(targetDate, uuid); // Audit Event evt = getFromEventSeries(auditTrailEvents.get(targetDate), uuid); if (evt != null) { return evt; } // FeatureUsage Map < String, EventSeries > maOfFeaturesIsages= featureUsageEvents.get(targetDate); if (maOfFeaturesIsages != null ) { for (EventSeries es : maOfFeaturesIsages.values()) { evt = getFromEventSeries(es, uuid); if (evt != null) { return evt; } } } // CheckOff Map < String, EventSeries > maOfChecKoff = checkOffEvents.get(targetDate); if (maOfChecKoff != null) { for (EventSeries es : maOfChecKoff.values()) { evt = getFromEventSeries(es, uuid); if (evt != null) { return evt; } } } return evt; } /** * Search event by its id in the eventSeries. * * @param es * current event series * @param uuid * current unique identifier * @return * event if found, null if not */ private Event getFromEventSeries(EventSeries es, String uuid) { if (es == null) return null; Iterator<Event> iterEvents = es.iterator(); while (iterEvents.hasNext()) { Event evt = iterEvents.next(); if (evt.getUuid().equalsIgnoreCase(uuid)) { return evt; } } return null; } }