package at.asitplus.eidas.specific.core.logger; import java.io.IOException; import java.time.LocalDateTime; import org.apache.commons.lang3.StringUtils; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import at.asitplus.eidas.specific.core.MsEidasNodeConstants; import at.asitplus.eidas.specific.modules.auth.eidas.v2.log.statistic.DetailedMatchtingStatistic; import at.asitplus.eidas.specific.modules.auth.eidas.v2.utils.MatchingTaskUtils; import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.api.IStatusMessenger; import at.gv.egiz.eaaf.core.api.idp.IAuthData; import at.gv.egiz.eaaf.core.api.logging.IStatisticLogger; import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** * Extended statistic logger that use a JSON based data model. * * @author tlenz * */ @Slf4j public class AdvancedStatisticLogger implements IStatisticLogger { private static final String DATEFORMATER = "yyyy.MM.dd-HH:mm:ss,SSS"; private static final String DEFAULT_NO_IDP_ID = "no idpId available"; private static final String DEFAULT_NO_SP_ID = "no appId available"; private static final ObjectMapper mapper = new ObjectMapper(); private static final JavaPropsMapper propsMapper = new JavaPropsMapper(); private final IStatusMessenger messageService; /** * Build a JSON based statistic logger that uses error messages from a specific source. * * @param source i18n message source */ public AdvancedStatisticLogger(IStatusMessenger source) { this.messageService = source; } @Override public void logSuccessOperation(IRequest protocolRequest, IAuthData authData, boolean isSsoSession) { final StatisticLogEntry entry = buildCoreEntry(protocolRequest); entry.setSuccess(SuccessEntry.builder() .spSector(protocolRequest.getServiceProviderConfiguration().getAreaSpecificTargetIdentifier()) .spCountry("AT") .citizenCountryCode(authData.getCiticenCountryCode()) .build()); entry.setMatching(MatchingDetails.builder() .matchingMethod(extractMatchingState(protocolRequest)) .matchingDetails(extractMatchingDetails(protocolRequest)) .build()); writeEntryToLog(entry); } @Override public void logErrorOperation(Throwable throwable) { log.trace("Advanced statistic logger only logs on SP level"); } @Override public void logErrorOperation(Throwable throwable, IRequest errorRequest) { final StatisticLogEntry entry = buildCoreEntry(errorRequest); entry.setError(new ErrorEntry(messageService.getResponseErrorCode(throwable), throwable.getMessage())); entry.setMatching(MatchingDetails.builder() .matchingMethod(extractMatchingState(errorRequest)) .matchingDetails(extractMatchingDetails(errorRequest)) .build()); writeEntryToLog(entry); } @Override public void internalTesting() throws Exception { log.trace("Not implemented for a File-based logger"); } private StatisticLogEntry buildCoreEntry(IRequest errorRequest) { Object appId = errorRequest.getRawData(MsEidasNodeConstants.DATA_REQUESTERID); return new StatisticLogEntry( LocalDateTime.now(), errorRequest.getUniqueTransactionIdentifier(), errorRequest.getServiceProviderConfiguration() != null && errorRequest.getServiceProviderConfiguration().getUniqueIdentifier() != null ? errorRequest.getServiceProviderConfiguration().getUniqueIdentifier() : DEFAULT_NO_IDP_ID, appId instanceof String && StringUtils.isNotEmpty((String)appId) ? (String)appId : DEFAULT_NO_SP_ID); } private String extractMatchingState(IRequest protocolRequest) { Object state = protocolRequest.getRawData(MsEidasNodeConstants.DATA_MATCHING_STATE); return state != null ? state.toString() : MsEidasNodeConstants.MatchingStates.NO_REQUIRED.toString(); } private DetailedMatchtingStatistic extractMatchingDetails(IRequest protocolRequest) { try { if (!MsEidasNodeConstants.MatchingStates.NO_REQUIRED.toString().equals( extractMatchingState(protocolRequest))) { return MatchingTaskUtils.getDetailedMatchingStatistic(protocolRequest); } } catch (EaafStorageException e) { log.warn("Can not generate detailed matching statistic", e); } return null; } private void writeEntryToLog(StatisticLogEntry entry) { try { log.info(mapper.writeValueAsString(propsMapper.writeValueAsProperties(entry))); } catch (final IOException e) { log.error("Can NOT generate statistic entry for logging", e); } } @Getter @Setter @RequiredArgsConstructor @JsonInclude(Include.NON_NULL) private static class StatisticLogEntry { @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonFormat(pattern = DATEFORMATER) @JsonProperty("timestamp") private final LocalDateTime timestamp; @JsonProperty("txId") private final String transactionId; @JsonProperty("entityid") private final String idpEntityId; @JsonProperty("unique-sp-id") private final String uniqueId; @JsonProperty("result") private SuccessEntry success; @JsonProperty("error") private ErrorEntry error; @JsonProperty("identityMatching") private MatchingDetails matching; } @Getter @Setter @Builder @JsonInclude(Include.NON_NULL) private static class SuccessEntry { @JsonProperty("spSector") private final String spSector; @JsonProperty("spCountry") private final String spCountry; @JsonProperty("citizenCountryCode") private final String citizenCountryCode; } @Getter @Setter @Builder @JsonInclude(Include.NON_NULL) private static class MatchingDetails { @JsonProperty("finalMatchingMethod") private final String matchingMethod; @JsonProperty("matchingProcessDetails") private final DetailedMatchtingStatistic matchingDetails; } @Getter @Setter @RequiredArgsConstructor private static class ErrorEntry { @JsonProperty("code") private final String errorCode; @JsonProperty("msg") private final String errorMessage; } }