/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation licenses this file to you 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 com.linecorp.armeria.server.logging.structured.kafka;
import static java.util.Objects.requireNonNull;
import java.util.Properties;
import java.util.concurrent.Future;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.linecorp.armeria.common.Request;
import com.linecorp.armeria.common.Response;
import com.linecorp.armeria.common.logging.RequestLog;
import com.linecorp.armeria.server.Service;
import com.linecorp.armeria.server.logging.structured.StructuredLogBuilder;
import com.linecorp.armeria.server.logging.structured.StructuredLoggingService;
/**
* Kafka backend for service log logging.
* This class enable Kafka as a service log logging backend.
*
* <p>This method returns immediately after the {@link Producer#send(ProducerRecord, Callback)} returns rather
* than waiting for returned {@link Future} completes so logs which are written and are not yet flushed can
* be lost if an application crashes in unclean way.
*
* <p>Refer variety of {@link #newDecorator} methods to see how to enable Kafka based structured logging.
*/
public class KafkaStructuredLoggingService<I extends Request, O extends Response, L>
extends StructuredLoggingService<I, O, L> {
private static final Logger logger = LoggerFactory.getLogger(KafkaStructuredLoggingService.class);
/**
* Implements "key" selector of Kafka based service log writer.
* Kafka as a notion of the "key" which is used as a criteria to guarantee message ordering and
* to achieve fine-distributed message partitioning.
* Users need to implement this interface in order to select arbitrary key from the request context
* or from the content included in a request.
*/
@FunctionalInterface
public interface KeySelector<E> {
/**
* Selects a key which should be associated toe the record given as {@code structuredLog}.
*
* @return A byte-array represented key or null
*/
@Nullable
byte[] selectKey(RequestLog log, E structuredLog);
}
/**
* Creates a decorator which provides {@link StructuredLoggingService} with full set of arguments.
*
* @param producer a kafka {@link Producer} producer which is used to send logs to Kafka
* @param topic a name of topic which is used to send logs
* @param logBuilder an instance of {@link StructuredLogBuilder} which is used to construct a log entry
* @param keySelector a {@link KeySelector} which is used to decide what key to use for the log
* @param <I> the {@link Request} type
* @param <O> the {@link Response} type
* @param <L> the type of the structured log representation
*
* @return a service decorator which adds structured logging support integrated to Kafka
*/
public static <I extends Request, O extends Response, L>
Function<Service<? super I, ? extends O>, StructuredLoggingService<I, O, L>> newDecorator(
Producer<byte[], L> producer, String topic,
StructuredLogBuilder<L> logBuilder, KeySelector<L> keySelector) {
return service -> new KafkaStructuredLoggingService<>(
service, logBuilder, producer, topic, keySelector, false);
}
/**
* Creates a decorator which provides {@link StructuredLoggingService} with defaulting key to null.
*
* @param producer a kafka {@link Producer} producer which is used to send logs to Kafka
* @param topic a name of topic which is used to send logs
* @param logBuilder an instance of {@link StructuredLogBuilder} which is used to construct a log entry
* @param <I> the {@link Request} type
* @param <O> the {@link Response} type
* @param <L> the type of the structured log representation
*
* @return a service decorator which adds structured logging support integrated to Kafka
*/
public static <I extends Request, O extends Response, L>
Function<Service<? super I, ? extends O>, StructuredLoggingService<I, O, L>> newDecorator(
Producer<byte[], L> producer, String topic,
StructuredLogBuilder<L> logBuilder) {
return newDecorator(producer, topic, logBuilder, null);
}
/**
* Creates a decorator which provides {@link StructuredLoggingService} with default {@link Producer}.
*
* @param bootstrapServers a {@code bootstrap.servers} config to specify destination Kafka cluster
* @param topic a name of topic which is used to send logs
* @param logBuilder an instance of {@link StructuredLogBuilder} which is used to construct a log entry
* @param keySelector a {@link KeySelector} which is used to decide what key to use for the log
* @param <I> the {@link Request} type
* @param <O> the {@link Response} type
* @param <L> the type of the structured log representation
*
* @return a service decorator which adds structured logging support integrated to Kafka
*/
public static <I extends Request, O extends Response, L>
Function<Service<? super I, ? extends O>, StructuredLoggingService<I, O, L>> newDecorator(
String bootstrapServers, String topic,
StructuredLogBuilder<L> logBuilder, KeySelector<L> keySelector) {
Producer<byte[], L> producer = new KafkaProducer<>(newDefaultConfig(bootstrapServers));
return service -> new KafkaStructuredLoggingService<>(
service, logBuilder, producer, topic, keySelector, true);
}
/**
* Creates a decorator which provides {@link StructuredLoggingService} with default {@link Producer}
* and defaulting key to null.
*
* @param bootstrapServers a {@code bootstrap.servers} config to specify destination Kafka cluster
* @param topic a name of topic which is used to send logs
* @param logBuilder an instance of {@link StructuredLogBuilder} which is used to construct a log entry
* @param <I> the {@link Request} type
* @param <O> the {@link Response} type
* @param <L> the type of the structured log representation
*
* @return a service decorator which adds structured logging support integrated to Kafka
*/
public static <I extends Request, O extends Response, L>
Function<Service<? super I, ? extends O>, StructuredLoggingService<I, O, L>>
newDecorator(String bootstrapServers, String topic, StructuredLogBuilder<L> logBuilder) {
return newDecorator(bootstrapServers, topic, logBuilder, null);
}
private static Properties newDefaultConfig(String bootstrapServers) {
Properties producerConfig = new Properties();
producerConfig.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
// Configure some values to make it likely fit majority of usages.
producerConfig.setProperty(ProducerConfig.CLIENT_ID_CONFIG,
KafkaStructuredLoggingService.class.getSimpleName());
producerConfig.setProperty(ProducerConfig.ACKS_CONFIG, "all");
producerConfig.setProperty(ProducerConfig.RETRIES_CONFIG, "3");
return producerConfig;
}
private final Producer<byte[], L> producer;
private final String topic;
private final KeySelector<L> keySelector;
private final boolean needToCloseProducer;
KafkaStructuredLoggingService(Service<? super I, ? extends O> delegate,
StructuredLogBuilder<L> logBuilder,
Producer<byte[], L> producer,
String topic,
@Nullable KeySelector<L> keySelector,
boolean needToCloseProducer) {
super(delegate, logBuilder);
this.producer = requireNonNull(producer, "producer");
this.topic = requireNonNull(topic, "topic");
this.keySelector = keySelector == null ? (res, log) -> null : keySelector;
this.needToCloseProducer = needToCloseProducer;
}
@Override
protected void writeLog(RequestLog log, L structuredLog) {
byte[] key = keySelector.selectKey(log, structuredLog);
ProducerRecord<byte[], L> producerRecord = new ProducerRecord<>(topic, key, structuredLog);
producer.send(producerRecord, (metadata, exception) -> {
if (exception != null) {
logger.warn("failed to send service log to Kafka {}", producerRecord, exception);
}
});
}
@Override
protected void close() {
if (needToCloseProducer) {
producer.close();
}
}
}