/* * Copyright 2014-2016 CyberVision, Inc. * * 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. */ package org.kaaproject.kaa.server.operations.service.akka.actors.core; import akka.actor.ActorContext; import akka.actor.ActorRef; import org.kaaproject.kaa.common.dto.EndpointProfileDataDto; import org.kaaproject.kaa.common.dto.EndpointProfileSchemaDto; import org.kaaproject.kaa.common.dto.ServerProfileSchemaDto; import org.kaaproject.kaa.common.dto.ctl.CTLSchemaDto; import org.kaaproject.kaa.server.common.dao.CtlService; import org.kaaproject.kaa.server.common.log.shared.appender.LogAppender; import org.kaaproject.kaa.server.common.log.shared.appender.LogDeliveryCallback; import org.kaaproject.kaa.server.common.log.shared.appender.LogDeliveryErrorCode; import org.kaaproject.kaa.server.common.log.shared.appender.LogSchema; import org.kaaproject.kaa.server.common.log.shared.appender.data.BaseLogEventPack; import org.kaaproject.kaa.server.common.log.shared.appender.data.BaseProfileInfo; import org.kaaproject.kaa.server.common.log.shared.appender.data.BaseSchemaInfo; import org.kaaproject.kaa.server.common.log.shared.appender.data.ProfileInfo; import org.kaaproject.kaa.server.common.thrift.gen.operations.Notification; import org.kaaproject.kaa.server.operations.service.akka.AkkaContext; import org.kaaproject.kaa.server.operations.service.akka.messages.core.logs.LogDeliveryMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.logs.LogEventPackMessage; import org.kaaproject.kaa.server.operations.service.akka.messages.core.logs.MultiLogDeliveryCallback; import org.kaaproject.kaa.server.operations.service.akka.messages.core.logs.SingleLogDeliveryCallback; import org.kaaproject.kaa.server.operations.service.cache.AppVersionKey; import org.kaaproject.kaa.server.operations.service.cache.CacheService; import org.kaaproject.kaa.server.operations.service.logs.LogAppenderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ApplicationLogActorMessageProcessor { private static final Logger LOG = LoggerFactory.getLogger(ApplicationLogActorMessageProcessor.class); private final LogAppenderService logAppenderService; private final CacheService cacheService; private final CtlService ctlService; private final Map<String, LogAppender> logAppenders; private final Map<LogAppenderFilterKey, List<LogAppender>> logAppendersCache; private final String applicationId; private final String applicationToken; private final Map<Integer, LogSchema> logSchemas; private final Map<AppVersionKey, BaseSchemaInfo> clientProfileSchemas; private final Map<AppVersionKey, BaseSchemaInfo> serverProfileSchemas; private final VoidCallback voidCallback; /** * Create a new instance of ApplicationLogActorMessageProcessor. * * @param context the akka context * @param applicationToken the application token */ public ApplicationLogActorMessageProcessor(AkkaContext context, String applicationToken) { super(); this.logAppenderService = context.getLogAppenderService(); this.cacheService = context.getCacheService(); this.ctlService = context.getCtlService(); this.applicationToken = applicationToken; this.applicationId = context.getApplicationService() .findAppByApplicationToken(applicationToken) .getId(); this.logAppenders = new HashMap<>(); this.logAppendersCache = new HashMap<>(); this.logSchemas = new HashMap<>(); this.clientProfileSchemas = new HashMap<>(); this.serverProfileSchemas = new HashMap<>(); this.voidCallback = new VoidCallback(); for (LogAppender appender : logAppenderService.getApplicationAppenders(applicationId)) { logAppenders.put(appender.getAppenderId(), appender); } } protected void processLogEventPack(ActorContext context, LogEventPackMessage message) { LOG.debug("[{}] Processing a log event pack with {} appenders", applicationToken, logAppenders.size()); fetchSchemas(message); LogSchema logSchema = message.getLogSchema(); List<LogAppender> required = filterAppenders(logSchema.getVersion(), true); List<LogAppender> optional = filterAppenders(logSchema.getVersion(), false); if (required.size() + optional.size() > 0) { optional.forEach(appender -> appender.doAppend(message.getLogEventPack(), voidCallback)); if (required.size() == 0) { sendSuccessMessageToEndpoint(message); } else { LogDeliveryCallback callback; if (required.size() == 1) { callback = new SingleLogDeliveryCallback( message.getOriginator(), message.getRequestId()); } else { callback = new MultiLogDeliveryCallback( message.getOriginator(), message.getRequestId(), required.size()); } required.forEach(appender -> { try { appender.doAppend(message.getLogEventPack(), callback); } catch (Exception cause) { String text = String.format("Failed to append logs using [%s] (ID: %s)", appender.getName(), appender.getAppenderId()); LOG.warn(text, cause); sendErrorMessageToEndpoint(message, LogDeliveryErrorCode.APPENDER_INTERNAL_ERROR); } }); } } else { sendErrorMessageToEndpoint(message, LogDeliveryErrorCode.NO_APPENDERS_CONFIGURED); } } /** * Sends a response to the endpoint. * * <p>Please note that this method was introduced purely as a workaround to * mocking an instance of {@link akka.actor.ActorRef}. Change the method * body with caution!</p> * * @param message A message to respond to */ protected void sendSuccessMessageToEndpoint(LogEventPackMessage message) { if (message.getOriginator() != null) { LogDeliveryMessage response = new LogDeliveryMessage(message.getRequestId(), true); message.getOriginator().tell(response, ActorRef.noSender()); } else { LOG.warn("[{}] Unable to respond to an unknown originator", applicationToken); } } /** * Filter appenders list by schema version and delivery confirmation. * * @param schemaVersion the schema version * @param confirmDelivery the confirm delivery * @return the list */ public List<LogAppender> filterAppenders(int schemaVersion, boolean confirmDelivery) { LogAppenderFilterKey key = new LogAppenderFilterKey(schemaVersion, confirmDelivery); List<LogAppender> result = logAppendersCache.get(key); if (result == null) { result = new ArrayList<LogAppender>(); for (LogAppender appender : logAppenders.values()) { if (appender.isSchemaVersionSupported(schemaVersion) && appender.isDeliveryConfirmationRequired() == confirmDelivery) { result.add(appender); } } } return result; } private void fetchSchemas(LogEventPackMessage message) { BaseLogEventPack logPack = message.getLogEventPack(); LogSchema logSchema = logPack.getLogSchema(); if (logSchema == null) { logSchema = logSchemas.get(message.getLogSchemaVersion()); if (logSchema == null) { logSchema = logAppenderService.getLogSchema(applicationId, logPack.getLogSchemaVersion()); logSchemas.put(message.getLogSchemaVersion(), logSchema); } logPack.setLogSchema(logSchema); } EndpointProfileDataDto profileDto = logPack.getProfileDto(); ProfileInfo clientProfile = logPack.getClientProfile(); if (clientProfile == null) { AppVersionKey key = new AppVersionKey( applicationToken, profileDto.getClientProfileVersion()); BaseSchemaInfo schemaInfo = clientProfileSchemas.get(key); if (schemaInfo == null) { EndpointProfileSchemaDto profileSchema = cacheService.getProfileSchemaByAppAndVersion(key); CTLSchemaDto ctlSchemaDto = cacheService.getCtlSchemaById(profileSchema.getCtlSchemaId()); String schema = ctlService.flatExportAsString(ctlSchemaDto); schemaInfo = new BaseSchemaInfo(ctlSchemaDto.getId(), schema); clientProfileSchemas.put(key, schemaInfo); } logPack.setClientProfile( new BaseProfileInfo(schemaInfo, profileDto.getClientProfileBody())); } ProfileInfo serverProfile = logPack.getServerProfile(); if (serverProfile == null) { AppVersionKey key = new AppVersionKey( applicationToken, profileDto.getServerProfileVersion()); BaseSchemaInfo schemaInfo = serverProfileSchemas.get(key); if (schemaInfo == null) { ServerProfileSchemaDto serverProfileSchema = cacheService.getServerProfileSchemaByAppAndVersion(key); CTLSchemaDto ctlSchemaDto = cacheService.getCtlSchemaById( serverProfileSchema.getCtlSchemaId()); String schema = ctlService.flatExportAsString(ctlSchemaDto); schemaInfo = new BaseSchemaInfo(ctlSchemaDto.getId(), schema); serverProfileSchemas.put(key, schemaInfo); } logPack.setServerProfile( new BaseProfileInfo(schemaInfo, profileDto.getServerProfileBody())); } } protected void sendErrorMessageToEndpoint(LogEventPackMessage message, LogDeliveryErrorCode errorCode) { if (message.getOriginator() != null) { message.getOriginator().tell(new LogDeliveryMessage( message.getRequestId(), false, errorCode), ActorRef.noSender()); } else { LOG.warn("[{}] Can't send error message to unknown originator.", applicationToken); } } protected void processLogAppenderNotification(Notification notification) { LOG.debug("Process log appender notification [{}]", notification); String appenderId = notification.getAppenderId(); switch (notification.getOp()) { case ADD_LOG_APPENDER: addLogAppender(appenderId); break; case REMOVE_LOG_APPENDER: removeLogAppender(appenderId); break; case UPDATE_LOG_APPENDER: removeLogAppender(appenderId); addLogAppender(appenderId); break; default: LOG.debug("[{}][{}] Operation [{}] is not supported.", applicationToken, appenderId, notification.getOp()); } } protected void stop() { for (LogAppender logAppender : logAppenders.values()) { LOG.info("[{}] Closing appender [{}] with name {}", applicationToken, logAppender.getAppenderId(), logAppender.getName()); logAppender.close(); } } private void addLogAppender(String appenderId) { LOG.info("[{}] Adding log appender with id [{}].", applicationId, appenderId); if (!logAppenders.containsKey(appenderId)) { LogAppender logAppender = logAppenderService.getApplicationAppender(appenderId); if (logAppender != null) { addAppender(appenderId, logAppender); LOG.info("[{}] Log appender [{}] registered.", applicationId, appenderId); } } else { LOG.info("[{}] Log appender [{}] is already registered.", applicationId, appenderId); } } private void removeLogAppender(String appenderId) { if (logAppenders.containsKey(appenderId)) { LOG.info("[{}] Closing log appender with id [{}].", applicationToken, appenderId); removeAppender(appenderId).close(); } else { LOG.warn("[{}] Can't remove unregistered appender with id [{}]", applicationToken, appenderId); } } private LogAppender removeAppender(String appenderId) { logAppendersCache.clear(); return logAppenders.remove(appenderId); } private void addAppender(String appenderId, LogAppender logAppender) { logAppendersCache.clear(); logAppenders.put(appenderId, logAppender); } protected static final class VoidCallback implements LogDeliveryCallback { @Override public void onSuccess() { // Do nothing } @Override public void onRemoteError() { // Do nothing } @Override public void onInternalError() { // Do nothing } @Override public void onConnectionError() { // Do nothing } } private static final class LogAppenderFilterKey { private int schemaVersion; private boolean confirmDelivery; private LogAppenderFilterKey(int schemaVersion, boolean confirmDelivery) { super(); this.schemaVersion = schemaVersion; this.confirmDelivery = confirmDelivery; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (confirmDelivery ? 1231 : 1237); result = prime * result + schemaVersion; return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } LogAppenderFilterKey other = (LogAppenderFilterKey) obj; if (confirmDelivery != other.confirmDelivery) { return false; } if (schemaVersion != other.schemaVersion) { return false; } return true; } } }