/***************************************************************************** * * Copyright (C) Zenoss, Inc. 2010-2011, all rights reserved. * * This content is made available according to terms specified in * License.zenoss under the directory where your Zenoss product is installed. * ****************************************************************************/ package org.zenoss.zep.rest; import org.jboss.resteasy.annotations.GZIP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zenoss.protobufs.ProtobufConstants; import org.zenoss.protobufs.util.Util.TimestampRange; import org.zenoss.protobufs.zep.Zep.EventDetailSet; import org.zenoss.protobufs.zep.Zep.EventFilter; import org.zenoss.protobufs.zep.Zep.EventNote; import org.zenoss.protobufs.zep.Zep.EventQuery; import org.zenoss.protobufs.zep.Zep.EventSeverity; import org.zenoss.protobufs.zep.Zep.EventSort; import org.zenoss.protobufs.zep.Zep.EventStatus; import org.zenoss.protobufs.zep.Zep.EventSummary; import org.zenoss.protobufs.zep.Zep.EventSummaryRequest; import org.zenoss.protobufs.zep.Zep.EventSummaryResult; import org.zenoss.protobufs.zep.Zep.EventSummaryUpdate; import org.zenoss.protobufs.zep.Zep.EventSummaryUpdateRequest; import org.zenoss.protobufs.zep.Zep.EventSummaryUpdateResponse; import org.zenoss.protobufs.zep.Zep.EventTagFilter; import org.zenoss.protobufs.zep.Zep.EventTagSeveritiesSet; import org.zenoss.protobufs.zep.Zep.FilterOperator; import org.zenoss.protobufs.zep.Zep.NumberRange; import org.zenoss.zep.PluginService; import org.zenoss.zep.ZepConstants; import org.zenoss.zep.ZepException; import org.zenoss.zep.dao.EventStoreDao; import org.zenoss.zep.index.EventIndexDao; import org.zenoss.zep.index.EventIndexer; import org.zenoss.zep.plugins.EventUpdateContext; import org.zenoss.zep.plugins.EventUpdatePlugin; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.Semaphore; import com.codahale.metrics.annotation.Timed; @Path("1.0/events") public class EventsResource { private static final Logger logger = LoggerFactory.getLogger(EventsResource.class); private int queryLimit = ZepConstants.DEFAULT_QUERY_LIMIT; private EventIndexer eventSummaryIndexer; private EventIndexer eventArchiveIndexer; private EventStoreDao eventStoreDao; private EventIndexDao eventSummaryIndexDao; private EventIndexDao eventArchiveIndexDao; private PluginService pluginService; private Semaphore archiveSemaphore = null; private int maxArchiveRequests = -1; public void setMaxArchiveRequests(int archiveRequestLimit) { if(archiveRequestLimit > 0) { this.maxArchiveRequests = archiveRequestLimit; this.archiveSemaphore = new Semaphore(this.maxArchiveRequests); logger.info("Limit of concurrent archive API requests set to {}", this.maxArchiveRequests); } } public void setQueryLimit(int limit) { if (limit > 0) { this.queryLimit = limit; } else { logger.warn("Invalid query limit: {}, using default: {}", limit, queryLimit); } } public void setEventStoreDao(EventStoreDao eventStoreDao) { this.eventStoreDao = eventStoreDao; } public void setEventSummaryIndexer(EventIndexer eventSummaryIndexer) { this.eventSummaryIndexer = eventSummaryIndexer; } public void setEventArchiveIndexer(EventIndexer eventArchiveIndexer) { this.eventArchiveIndexer = eventArchiveIndexer; } public void setEventSummaryIndexDao(EventIndexDao eventSummaryIndexDao) { this.eventSummaryIndexDao = eventSummaryIndexDao; } public void setEventArchiveIndexDao(EventIndexDao eventArchiveIndexDao) { this.eventArchiveIndexDao = eventArchiveIndexDao; } public void setPluginService(PluginService pluginService) { this.pluginService = pluginService; } private static Set<String> getQuerySet(MultivaluedMap<String, String> params, String name) { final Set<String> set; final List<String> l = params.get(name); if (l == null) { set = Collections.emptySet(); } else { set = new HashSet<String>(l.size()); set.addAll(l); } return set; } private static int getQueryInteger(MultivaluedMap<String, String> params, String name, int defaultValue) { final String strVal = params.getFirst(name); return (strVal != null) ? Integer.valueOf(strVal) : defaultValue; } static <T extends Enum<T>> EnumSet<T> getQueryEnumSet(MultivaluedMap<String, String> params, String name, Class<T> enumClass, String prefix) { final EnumSet<T> set = EnumSet.noneOf(enumClass); final List<String> values = params.get(name); if (values != null) { for (String value : values) { value = value.toUpperCase(); if (!value.startsWith(prefix)) { value = prefix + value; } set.add(Enum.valueOf(enumClass, value)); } } return set; } @POST @Path("search") @Consumes({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response createSavedSearch(EventQuery query, @Context UriInfo ui) throws URISyntaxException, ZepException { return createSavedSearchInternal(this.eventSummaryIndexDao, query, ui); } @POST @Path("archive/search") @Consumes({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response createArchiveSavedSearch(EventQuery query, @Context UriInfo ui) throws URISyntaxException, ZepException { return createSavedSearchInternal(this.eventArchiveIndexDao, query, ui); } public Response createSavedSearchInternal(EventIndexDao indexDao, EventQuery query, @Context UriInfo ui) throws URISyntaxException, ZepException { // Make sure index is up to date with latest events prior to creating the saved search eventSummaryIndexer.indexFully(); String uuid = indexDao.createSavedSearch(query); return Response.created(new URI(ui.getRequestUri().toString() + '/' + uuid)).build(); } @GET @Path("search/{searchUuid}") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response listSavedSearch(@PathParam("searchUuid") String searchUuid, @QueryParam("offset") String offsetStr, @QueryParam("limit") String limitStr) throws ZepException { return listSavedSearchInternal(this.eventSummaryIndexDao, searchUuid, offsetStr, limitStr); } @GET @Path("archive/search/{searchUuid}") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response listArchiveSavedSearch(@PathParam("searchUuid") String searchUuid, @QueryParam("offset") String offsetStr, @QueryParam("limit") String limitStr) throws ZepException { return listSavedSearchInternal(this.eventArchiveIndexDao, searchUuid, offsetStr, limitStr); } private Response listSavedSearchInternal(EventIndexDao indexDao, String searchUuid, String offsetStr, String limitStr) throws ZepException { int offset = 0; if (offsetStr != null) { offset = Integer.parseInt(offsetStr); if (offset < 0) { throw new ZepException("Invalid offset: " + offsetStr); } } int limit = queryLimit; if (limitStr != null) { limit = Integer.parseInt(limitStr); if (limit > queryLimit) { limit = queryLimit; } } Response response; try { response = Response.ok(indexDao.savedSearch(searchUuid, offset, limit)).build(); } catch (ZepException e) { if (e.getLocalizedMessage() != null && e.getLocalizedMessage().startsWith("ZEP0001E")) { response = Response.status(Status.NOT_FOUND).entity(e.getLocalizedMessage()).type(MediaType.TEXT_PLAIN_TYPE).build(); } else { throw e; } } return response; } @DELETE @Path("search/{searchUuid}") @GZIP @Timed public Response deleteSavedSearch(@PathParam("searchUuid") String searchUuid) throws ZepException { return deleteSavedSearchInternal(this.eventSummaryIndexDao, searchUuid); } @DELETE @Path("archive/search/{searchUuid}") @GZIP @Timed public Response deleteArchiveSavedSearch(@PathParam("searchUuid") String searchUuid) throws ZepException { return deleteSavedSearchInternal(this.eventArchiveIndexDao, searchUuid); } private Response deleteSavedSearchInternal(EventIndexDao indexDao, String searchUuid) throws ZepException { String uuid = indexDao.deleteSavedSearch(searchUuid); if (uuid == null) { return Response.status(Status.NOT_FOUND).build(); } return Response.status(Status.NO_CONTENT).build(); } @PUT @Path("search/{searchUuid}") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public EventSummaryUpdateResponse updateEvents(@PathParam("searchUuid") String searchUuid, EventSummaryUpdateRequest request) throws ZepException { if (request.hasEventQueryUuid() && !searchUuid.equals(request.getEventQueryUuid())) { throw new ZepException(String.format("Mismatched search UUIDs: '%s' != '%s'", searchUuid, request.getEventQueryUuid())); } EventSummaryResult result = this.eventSummaryIndexDao.savedSearchUuids(searchUuid, request.getOffset(), request.getLimit()); List<String> uuids = new ArrayList<String>(result.getEventsCount()); for (EventSummary summary : result.getEventsList()) { uuids.add(summary.getUuid()); } EventSummaryUpdate update = request.getUpdateFields(); int numUpdated = this.eventStoreDao.update(uuids, update); if (numUpdated > 0) { eventSummaryIndexer.indexFully(); EventUpdateContext context = new EventUpdateContext() { }; for (EventUpdatePlugin plugin : pluginService.getPluginsByType(EventUpdatePlugin.class)) { plugin.onStatusUpdate(uuids, update, context); } } EventSummaryUpdateResponse.Builder response = EventSummaryUpdateResponse.newBuilder(); if (result.hasNextOffset()) { EventSummaryUpdateRequest.Builder requestBuilder = EventSummaryUpdateRequest.newBuilder(request); // Must set limit again in case initial limit is greater than configured maximum requestBuilder.setOffset(result.getNextOffset()).setLimit(result.getLimit()); requestBuilder.setEventQueryUuid(searchUuid); response.setNextRequest(requestBuilder.build()); } response.setTotal(result.getTotal()); response.setUpdated(numUpdated); return response.build(); } @GET @Path("{eventUuid}") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response getEventSummaryByUuid(@PathParam("eventUuid") String eventUuid) throws ZepException { EventSummary summary = eventStoreDao.findByUuid(eventUuid); if (summary == null) { return Response.status(Status.NOT_FOUND).build(); } return Response.ok(summary).build(); } @PUT @Path("{eventUuid}") @Consumes({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response updateEventSummaryByUuid(@PathParam("eventUuid") String uuid, EventSummaryUpdate update) throws ZepException { EventSummary summary = eventStoreDao.findByUuid(uuid); if (summary == null) { return Response.status(Status.NOT_FOUND).build(); } int numRows = eventStoreDao.update(summary.getUuid(), update); if (numRows > 0) { eventSummaryIndexer.indexFully(); } return Response.noContent().build(); } @POST @Path("{eventUuid}/notes") @Consumes({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response addNote(@PathParam("eventUuid") String eventUuid, EventNote note) throws ZepException { EventSummary summary = eventStoreDao.findByUuid(eventUuid); if (summary == null) { return Response.status(Status.NOT_FOUND).build(); } int numRows = eventStoreDao.addNote(summary.getUuid(), note); if (numRows == 0) { return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } EventUpdateContext context = new EventUpdateContext() { }; for (EventUpdatePlugin plugin : pluginService.getPluginsByType(EventUpdatePlugin.class)) { plugin.onNoteAdd(eventUuid, note, context); } return Response.noContent().build(); } @POST @Path("notes_async") @Consumes({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response addNoteBulkAsync(@QueryParam("uuid") Set<String> uuids, EventNote note) throws ZepException { if (uuids == null) return Response.noContent().build(); final Set<EventSummary> summaries = new HashSet<EventSummary>(uuids.size()); for (String uuid : uuids) { EventSummary summary = eventStoreDao.findByUuid(uuid); if (summary == null) { return Response.status(Status.NOT_FOUND).build(); } summaries.add(summary); } final EventUpdateContext context = new EventUpdateContext() {}; for (EventSummary summary : summaries) { final String uuid = summary.getUuid(); int numRows = eventStoreDao.addNote(uuid, note); if (numRows == 0) { return Response.status(Status.INTERNAL_SERVER_ERROR).build(); } for (EventUpdatePlugin plugin : pluginService.getPluginsByType(EventUpdatePlugin.class)) { plugin.onNoteAdd(uuid, note, context); } } return Response.noContent().build(); } @POST @Path("/") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public EventSummaryResult listEventIndex(EventSummaryRequest request) throws ZepException { return this.eventSummaryIndexDao.list(request); } private EventSummaryResult getEventArchiveResults(EventSummaryRequest request) throws ZepException { EventSummaryResult result = null; if (this.archiveSemaphore == null || this.archiveSemaphore.tryAcquire()) { try { result = this.eventArchiveIndexDao.list(request); } catch (ZepException ze) { throw ze; } catch (Exception e) { throw new ZepException(e.getLocalizedMessage(), e); } finally { if (this.archiveSemaphore != null) this.archiveSemaphore.release(); } } else { String msg = "Too many API archive requests. Limit = " + this.maxArchiveRequests; logger.warn(msg); throw new ZepException(msg); } return result; } @POST @Path("archive") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public EventSummaryResult listEventIndexArchive(EventSummaryRequest request) throws ZepException { return this.getEventArchiveResults(request); } @GET @Path("/") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public EventSummaryResult listEventIndexGet(@Context UriInfo ui) throws ParseException, ZepException { return this.eventSummaryIndexDao.list(eventSummaryRequestFromUriInfo(ui)); } @GET @Path("archive") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public EventSummaryResult listEventIndexArchiveGet(@Context UriInfo ui) throws ParseException, ZepException { return this.getEventArchiveResults(eventSummaryRequestFromUriInfo(ui)); } @POST @Path("tag_severities") @Produces({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public EventTagSeveritiesSet getEventTagSeverities(EventFilter filter) throws ZepException { return this.eventSummaryIndexDao.getEventTagSeverities(filter); } @POST @Path("{eventUuid}/details") @Consumes({ MediaType.APPLICATION_JSON, ProtobufConstants.CONTENT_TYPE_PROTOBUF }) @GZIP @Timed public Response updateEventDetails(@PathParam("eventUuid") String eventUuid, EventDetailSet details) throws ZepException { logger.debug("updateEventDetails Enter"); int numRows = eventStoreDao.updateDetails(eventUuid, details); if (numRows == 0) { return Response.status(Status.NOT_FOUND).build(); } else { EventUpdateContext context = new EventUpdateContext() { }; for (EventUpdatePlugin plugin : pluginService.getPluginsByType(EventUpdatePlugin.class)) { plugin.onEventDetailUpdate(eventUuid, details, context); } } return Response.noContent().build(); } private EventSummaryRequest eventSummaryRequestFromUriInfo(UriInfo info) throws ParseException { /* Read all params from query */ MultivaluedMap<String, String> queryParams = info.getQueryParameters(); logger.debug("Query Parameters: {}", queryParams); final int limit = getQueryInteger(queryParams, "limit", queryLimit); final int offset = getQueryInteger(queryParams, "offset", 0); final Set<String> sorts = getQuerySet(queryParams, "sort"); /* Build event request */ final EventSummaryRequest.Builder reqBuilder = EventSummaryRequest .newBuilder(); reqBuilder.setEventFilter(createEventFilter(queryParams, false)); reqBuilder.setExclusionFilter(createEventFilter(queryParams, true)); if (limit < 0) { throw new IllegalArgumentException("Invalid limit: " + limit); } reqBuilder.setLimit(limit); if (offset < 0) { throw new IllegalArgumentException("Invalid offset: " + offset); } reqBuilder.setOffset(offset); for (String sort : sorts) { String[] sortParts = sort.split(",", 2); EventSort.Builder eventSort = EventSort.newBuilder(); eventSort.setField(EventSort.Field.valueOf(sortParts[0])); if (sortParts.length == 2) { eventSort.setDirection(EventSort.Direction.valueOf(sortParts[1])); } reqBuilder.addSort(eventSort); } return reqBuilder.build(); } static EventFilter createEventFilter(MultivaluedMap<String, String> queryParams, boolean isExclusion) throws ParseException { String prefix = (isExclusion) ? "ex_" : ""; final EnumSet<EventSeverity> severities = getQueryEnumSet(queryParams, prefix + "severity", EventSeverity.class, "SEVERITY_"); final EnumSet<EventStatus> status = getQueryEnumSet(queryParams, prefix + "status", EventStatus.class, "STATUS_"); final Set<String> eventClass = getQuerySet(queryParams, prefix + "event_class"); final TimestampRange firstSeen = parseRange(queryParams.getFirst(prefix + "first_seen")); final TimestampRange lastSeen = parseRange(queryParams.getFirst(prefix + "last_seen")); final TimestampRange statusChange = parseRange(queryParams.getFirst(prefix + "status_change")); final TimestampRange updateTime = parseRange(queryParams.getFirst(prefix + "update_time")); final NumberRange count = convertCount(queryParams.getFirst(prefix + "count")); final Set<String> fingerprint = getQuerySet(queryParams, prefix + "fingerprint"); final Set<String> element_identifier = getQuerySet(queryParams, prefix + "element_identifier"); final Set<String> element_sub_identifier = getQuerySet(queryParams, prefix + "element_sub_identifier"); final Set<String> uuids = getQuerySet(queryParams, prefix + "uuid"); final Set<String> summary = getQuerySet(queryParams, prefix + "event_summary"); final Set<String> current_user = getQuerySet(queryParams, prefix + "current_user"); final Set<String> tagUuids = getQuerySet(queryParams, prefix + "tag_uuids"); final String tagUuidsOp = queryParams.getFirst(prefix + "tag_uuids_op"); // TODO: EventDetailFilter /* Build event filter */ final EventFilter.Builder filterBuilder = EventFilter.newBuilder(); filterBuilder.addAllFingerprint(fingerprint); filterBuilder.addAllSeverity(severities); filterBuilder.addAllStatus(status); filterBuilder.addAllEventClass(eventClass); filterBuilder.addAllElementIdentifier(element_identifier); filterBuilder.addAllElementSubIdentifier(element_sub_identifier); filterBuilder.addAllUuid(uuids); filterBuilder.addAllEventSummary(summary); filterBuilder.addAllCurrentUserName(current_user); if (firstSeen != null) { filterBuilder.addFirstSeen(firstSeen); } if (lastSeen != null) { filterBuilder.addLastSeen(lastSeen); } if (statusChange != null) { filterBuilder.addStatusChange(statusChange); } if (updateTime != null) { filterBuilder.addUpdateTime(updateTime); } if (count != null) { filterBuilder.addCountRange(count); } if (!tagUuids.isEmpty()) { FilterOperator op = FilterOperator.OR; if (tagUuidsOp != null) { op = FilterOperator.valueOf(tagUuidsOp.toUpperCase()); } EventTagFilter.Builder tagFilterBuilder = EventTagFilter.newBuilder(); tagFilterBuilder.addAllTagUuids(tagUuids); tagFilterBuilder.setOp(op); filterBuilder.addTagFilter(tagFilterBuilder.build()); } return filterBuilder.build(); } static TimestampRange parseRange(String range) throws ParseException { if (range == null) { return null; } final TimestampRange.Builder builder = TimestampRange.newBuilder(); final int solidusIndex = range.indexOf('/'); if (solidusIndex != -1) { final String start = range.substring(0, solidusIndex); final String end = range.substring(solidusIndex + 1); final long startTs = parseISO8601(start); final long endTs = parseISO8601(end); if (startTs > endTs) { throw new IllegalArgumentException(start + " > " + end); } builder.setStartTime(startTs).setEndTime(endTs); } else { builder.setStartTime(parseISO8601(range)); } return builder.build(); } static long parseISO8601(String str) throws ParseException { final SimpleDateFormat fmt; if (str.indexOf('.') > 0) { fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); } else { fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); } fmt.setTimeZone(TimeZone.getTimeZone("UTC")); return fmt.parse(str).getTime(); } static NumberRange convertCount(String count) { if (count == null || count.isEmpty()) { return null; } Integer from = null, to = null; final int colon = count.indexOf(':'); // RANGE in form of FROM:TO if (colon != -1) { String strFrom = count.substring(0, colon); String strTo = count.substring(colon+1); if (!strFrom.isEmpty()) { from = Integer.parseInt(strFrom); } if (!strTo.isEmpty()) { to = Integer.parseInt(strTo); } } // [GT|GTEQ|LT|LTEQ|EQ]NUM else { switch (count.charAt(0)) { case '>': if (count.charAt(1) == '=') { from = Integer.parseInt(count.substring(2)); } else { from = Integer.parseInt(count.substring(1)) + 1; } break; case '<': if (count.charAt(1) == '=') { to = Integer.parseInt(count.substring(2)); } else { to = Integer.parseInt(count.substring(1)) - 1; } break; case '=': from = to = Integer.parseInt(count.substring(1)); break; default: from = to = Integer.parseInt(count); break; } } if (from == null && to == null) { return null; } if (from != null && to != null && from > to) { throw new IllegalArgumentException("Count from > to: " + from + "," + to); } final NumberRange.Builder rangeBuilder = NumberRange.newBuilder(); if (from != null) { if (from < 0) { throw new IllegalArgumentException("Count number out of range: " + from); } rangeBuilder.setFrom(from); } if (to != null) { if (to < 0) { throw new IllegalArgumentException("Count number out of range: " + to); } rangeBuilder.setTo(to); } return rangeBuilder.build(); } }