/*
* 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.appenders.rest.appender;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.kaaproject.kaa.common.dto.logs.LogAppenderDto;
import org.kaaproject.kaa.common.dto.logs.LogEventDto;
import org.kaaproject.kaa.server.appenders.rest.config.gen.MethodType;
import org.kaaproject.kaa.server.appenders.rest.config.gen.RequestType;
import org.kaaproject.kaa.server.appenders.rest.config.gen.RestConfig;
import org.kaaproject.kaa.server.common.log.shared.appender.AbstractLogAppender;
import org.kaaproject.kaa.server.common.log.shared.appender.LogDeliveryCallback;
import org.kaaproject.kaa.server.common.log.shared.appender.LogEventPack;
import org.kaaproject.kaa.server.common.log.shared.avro.gen.RecordHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class RestLogAppender extends AbstractLogAppender<RestConfig> {
private static final Logger LOG = LoggerFactory.getLogger(RestLogAppender.class);
private ExecutorService executor;
private CloseableHttpClient client;
private HttpHost target;
private URI targetUri;
private RestConfig configuration;
private boolean closed = false;
public RestLogAppender() {
super(RestConfig.class);
}
@Override
protected void initFromConfiguration(LogAppenderDto appender, RestConfig configuration) {
this.configuration = configuration;
this.executor = Executors.newFixedThreadPool(configuration.getConnectionPoolSize());
target = new HttpHost(configuration.getHost(),
configuration.getPort(), configuration.getSsl() ? "https" : "http");
HttpClientBuilder builder = HttpClients.custom();
if (configuration.getUsername() != null && configuration.getPassword() != null) {
LOG.info("Adding basic auth credentials provider");
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(target.getHostName(), target.getPort()),
new UsernamePasswordCredentials(configuration.getUsername(), configuration.getPassword())
);
builder.setDefaultCredentialsProvider(credsProvider);
}
if (!configuration.getVerifySslCert()) {
LOG.info("Adding trustful ssl context");
SSLContextBuilder sslBuilder = new SSLContextBuilder();
try {
sslBuilder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslBuilder.build());
builder.setSSLSocketFactory(sslsf);
} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException ex) {
LOG.error("Failed to init socket factory {}", ex.getMessage(), ex);
}
}
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setDefaultMaxPerRoute(configuration.getConnectionPoolSize());
cm.setMaxTotal(configuration.getConnectionPoolSize());
builder.setConnectionManager(cm);
this.client = builder.build();
}
@Override
public void doAppend(LogEventPack logEventPack, RecordHeader header,
final LogDeliveryCallback listener) {
if (closed) {
LOG.warn("Attempt to append data to already stopped appender");
listener.onInternalError();
}
if (targetUri == null) {
try {
targetUri = new URIBuilder()
.setHost(target.getHostName())
.setPort(target.getPort())
.setScheme(target.getSchemeName())
.setPath(configuration.getPath())
.build();
} catch (URISyntaxException ex) {
LOG.warn("[{}] failed to build request URI", this.getApplicationToken(), ex);
listener.onInternalError();
}
}
LOG.trace("[{}] appending {} logs to rest endpoint",
this.getApplicationToken(), logEventPack.getEvents().size());
final RestConfig configuration = this.configuration;
try {
for (final LogEventDto dto : generateLogEvent(logEventPack, header)) {
executor.submit(new Runnable() {
@Override
public void run() {
try {
LOG.trace("[{}] appending {} to rest endpoint",
RestLogAppender.this.getApplicationToken(), dto);
final HttpRequest request = createRequest(configuration, dto);
LOG.trace("[{}] executing {}",
RestLogAppender.this.getApplicationToken(), request.getRequestLine());
CloseableHttpResponse response = client.execute(target, request);
try {
int responseCode = response.getStatusLine().getStatusCode();
LOG.trace("[{}] received {} response code",
RestLogAppender.this.getApplicationToken(), response);
if (responseCode >= 200 && responseCode < 400) {
LOG.trace("[{}] logs appended successfully", getName());
listener.onSuccess();
} else {
LOG.warn("[{}] bad response code {}", getName(), responseCode);
listener.onRemoteError();
}
} finally {
response.close();
}
} catch (IOException ex) {
LOG.error("[{}] Failed to send log event.", getName(), ex);
listener.onConnectionError();
} catch (Exception ex) {
LOG.error("[{}] Failed to send log event.", getName(), ex);
listener.onInternalError();
}
}
});
}
} catch (IOException ex) {
LOG.error("[{}] Failed to send log events.", getName(), ex);
listener.onInternalError();
}
}
private HttpRequest createRequest(RestConfig configuration, LogEventDto dto)
throws URISyntaxException {
String body = buildRequestBody(configuration, dto);
ContentType contentType = buildContentType(configuration);
StringEntity entity = new StringEntity(body, contentType);
final HttpEntityEnclosingRequestBase request;
if (configuration.getMethod() == MethodType.POST) {
request = new HttpPost(targetUri);
} else {
request = new HttpPut(targetUri);
}
request.setEntity(entity);
return request;
}
private ContentType buildContentType(RestConfig configuration) {
ContentType contentType;
if (configuration.getMimeType() == RequestType.TEXT) {
contentType = ContentType.create("text/plain", "UTF-8");
} else {
contentType = ContentType.create("application/json", "UTF-8");
}
return contentType;
}
private String buildRequestBody(RestConfig configuration, LogEventDto dto) {
String body;
if (configuration.getHeader()) {
StringBuilder sb = new StringBuilder();
sb.append("{\"header\":").append(dto.getHeader()).append(",");
sb.append("\"event\":").append(dto.getEvent()).append("}");
body = sb.toString();
} else {
body = dto.getEvent();
}
return body;
}
@Override
public void close() {
closed = true;
try {
client.close();
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (IOException | InterruptedException ex) {
LOG.error("Failed to close appender: {}", ex.getMessage(), ex);
}
}
}