
io.questdb.log.LogAlertSocketWriter Maven / Gradle / Ivy
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2023 QuestDB
*
* 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 io.questdb.log;
import io.questdb.mp.QueueConsumer;
import io.questdb.mp.RingQueue;
import io.questdb.mp.SCSequence;
import io.questdb.mp.SynchronizedJob;
import io.questdb.network.NetworkFacade;
import io.questdb.network.NetworkFacadeImpl;
import io.questdb.std.*;
import io.questdb.std.datetime.microtime.MicrosecondClock;
import io.questdb.std.datetime.microtime.MicrosecondClockImpl;
import io.questdb.std.str.CharSink;
import io.questdb.std.str.Path;
import io.questdb.std.str.StringSink;
import org.jetbrains.annotations.TestOnly;
import java.io.Closeable;
import java.io.InputStream;
import static io.questdb.log.TemplateParser.TemplateNode;
public class LogAlertSocketWriter extends SynchronizedJob implements Closeable, LogWriter {
public static final CharSequenceObjHashMap ALERT_PROPS = TemplateParser.adaptMap(System.getenv());
public static final String DEFAULT_ALERT_TPT_FILE = "/alert-manager-tpt.json";
private static final String CLUSTER_ENV = "CLUSTER_NAME";
private static final String DEFAULT_ENV_VALUE = "GLOBAL";
private static final String INSTANCE_ENV = "INSTANCE_NAME";
private static final String MESSAGE_ENV = "ALERT_MESSAGE";
private static final String MESSAGE_ENV_VALUE = "${" + MESSAGE_ENV + "}";
private static final String NAMESPACE_ENV = "NAMESPACE";
private static final String ORG_ID_ENV = "ORGID";
private final TemplateParser alertTemplate = new TemplateParser();
private final RingQueue alertsSourceQueue;
private final MicrosecondClock clock;
private final FilesFacade ff;
private final int level;
private final NetworkFacade nf;
private final CharSequenceObjHashMap properties;
private final StringSink sink = new StringSink();
private final SCSequence writeSequence;
private HttpLogRecordSink alertSink;
private String alertTargets;
private ObjList alertTemplateNodes;
private int alertTemplateNodesLen;
// changed by introspection
private String defaultAlertHost;
private String defaultAlertPort;
private String inBufferSize;
private String location;
private Log log;
private String outBufferSize;
private String reconnectDelay;
private LogAlertSocket socket;
private final QueueConsumer alertsProcessor = this::onLogRecord;
public LogAlertSocketWriter(RingQueue alertsSrc, SCSequence writeSequence, int level) {
this(
FilesFacadeImpl.INSTANCE,
NetworkFacadeImpl.INSTANCE,
MicrosecondClockImpl.INSTANCE,
alertsSrc,
writeSequence,
level,
ALERT_PROPS
);
}
public LogAlertSocketWriter(
FilesFacade ff,
NetworkFacade nf,
MicrosecondClock clock,
RingQueue alertsSrc,
SCSequence writeSequence,
int level,
CharSequenceObjHashMap properties
) {
this.ff = ff;
this.nf = nf;
this.clock = clock;
this.alertsSourceQueue = alertsSrc;
this.writeSequence = writeSequence;
this.level = level & ~(1 << Numbers.msb(LogLevel.ADVISORY)); // switch off ADVISORY
this.properties = properties;
}
@TestOnly
public static void readFile(String location, long address, long addressSize, FilesFacade ff, CharSink sink) {
int templateFd = -1;
try (Path path = new Path()) {
// Paths for logger are typically derived from resources.
// They may start with `/C:` on Windows OS, which is Java way of emphasising absolute path.
// We have to remove `/` in that path before calling native methods.
if (Os.isWindows() && location.charAt(0) == '/') {
path.of(location, 1, location.length());
} else {
path.of(location);
}
templateFd = ff.openRO(path.$());
if (templateFd == -1) {
throw new LogError(String.format(
"Cannot read %s [errno=%d]",
location,
ff.errno()
));
}
long size = ff.length(templateFd);
if (size > addressSize) {
throw new LogError("Template file is too big");
}
if (size < 0 || size != ff.read(templateFd, address, size, 0)) {
throw new LogError(String.format(
"Cannot read %s [errno=%d, size=%d]",
location,
ff.errno(),
size
));
}
Chars.utf8toUtf16(address, address + size, sink);
} finally {
ff.close(templateFd);
}
}
@Override
public void bindProperties(LogFactory factory) {
int nInBufferSize = LogAlertSocket.IN_BUFFER_SIZE;
if (inBufferSize != null) {
try {
nInBufferSize = Numbers.parseIntSize(inBufferSize);
} catch (NumericException e) {
throw new LogError("Invalid value for inBufferSize: " + inBufferSize);
}
}
int nOutBufferSize = LogAlertSocket.OUT_BUFFER_SIZE;
if (outBufferSize != null) {
try {
nOutBufferSize = Numbers.parseIntSize(outBufferSize);
} catch (NumericException e) {
throw new LogError("Invalid value for outBufferSize: " + outBufferSize);
}
}
long nReconnectDelay = LogAlertSocket.RECONNECT_DELAY_NANO;
if (reconnectDelay != null) {
try {
nReconnectDelay = Numbers.parseLong(reconnectDelay) * 1000000; // config is in milli
} catch (NumericException e) {
throw new LogError("Invalid value for reconnectDelay: " + reconnectDelay);
}
}
if (defaultAlertHost == null) {
defaultAlertHost = LogAlertSocket.DEFAULT_HOST;
}
int nDefaultPort = LogAlertSocket.DEFAULT_PORT;
if (defaultAlertPort != null) {
try {
nDefaultPort = Numbers.parseInt(defaultAlertPort);
} catch (NumericException e) {
throw new LogError("Invalid value for defaultAlertPort: " + defaultAlertPort);
}
}
log = factory.create(LogAlertSocketWriter.class.getName());
socket = new LogAlertSocket(
nf,
alertTargets,
nInBufferSize,
nOutBufferSize,
nReconnectDelay,
defaultAlertHost,
nDefaultPort,
log
);
alertSink = new HttpLogRecordSink(socket)
.putHeader(LogAlertSocket.localHostIp)
.setMark();
loadLogAlertTemplate();
socket.connect();
}
@Override
public void close() {
Misc.free(socket);
}
@TestOnly
public HttpLogRecordSink getAlertSink() {
return alertSink;
}
@TestOnly
public String getAlertTargets() {
return socket.getAlertTargets();
}
@TestOnly
public String getDefaultAlertHost() {
return socket.getDefaultAlertHost();
}
@TestOnly
public int getDefaultAlertPort() {
return socket.getDefaultAlertPort();
}
@TestOnly
public int getInBufferSize() {
return socket.getInBufferSize();
}
@TestOnly
public String getLocation() {
return location;
}
@TestOnly
public int getOutBufferSize() {
return socket.getOutBufferSize();
}
@TestOnly
public long getReconnectDelay() {
return socket.getReconnectDelay();
}
@TestOnly
public void onLogRecord(LogRecordSink logRecord) {
final int len = logRecord.length();
if ((logRecord.getLevel() & level) != 0 && len > 0) {
alertTemplate.setDateValue(clock.getTicks());
alertSink.rewindToMark();
for (int i = 0; i < alertTemplateNodesLen; i++) {
TemplateNode comp = alertTemplateNodes.getQuick(i);
if (comp.isEnv(MESSAGE_ENV)) {
alertSink.put(logRecord);
} else {
alertSink.put(comp);
}
}
sink.clear();
sink.put(logRecord);
sink.clear(sink.length() - Misc.EOL.length());
log.info().$("Sending: ").$(sink).$();
socket.send(alertSink.$());
}
}
@Override
public boolean runSerially() {
return writeSequence.consumeAll(alertsSourceQueue, alertsProcessor);
}
@TestOnly
public void setAlertTargets(String alertTargets) {
this.alertTargets = alertTargets;
}
@TestOnly
public void setDefaultAlertHost(String defaultAlertHost) {
this.defaultAlertHost = defaultAlertHost;
}
@TestOnly
public void setDefaultAlertPort(String defaultAlertPort) {
this.defaultAlertPort = defaultAlertPort;
}
@TestOnly
public void setInBufferSize(String inBufferSize) {
this.inBufferSize = inBufferSize;
}
@TestOnly
public void setLocation(String location) {
this.location = location;
}
@TestOnly
public void setOutBufferSize(String outBufferSize) {
this.outBufferSize = outBufferSize;
}
@TestOnly
public void setReconnectDelay(String reconnectDelay) {
this.reconnectDelay = reconnectDelay;
}
private void loadLogAlertTemplate() {
final long now = clock.getTicks();
if (location == null || location.isEmpty()) {
location = DEFAULT_ALERT_TPT_FILE;
}
location = alertTemplate.parseEnv(location, now).toString(); // location may contain dollar expressions
// read template, resolve env vars within (except $ALERT_MESSAGE)
boolean needsReading = true;
try (InputStream is = LogAlertSocketWriter.class.getResourceAsStream(location)) {
if (is != null) {
byte[] buff = new byte[LogAlertSocket.IN_BUFFER_SIZE];
int len = is.read(buff, 0, buff.length);
String template = new String(buff, 0, len, Files.UTF_8);
alertTemplate.parse(template, now, properties);
needsReading = false;
}
} catch (LogError e) {
throw e;
} catch (Throwable e) {
// it was not a resource ("/resource_name")
}
if (needsReading) {
sink.clear();
readFile(
location,
socket.getInBufferPtr(),
socket.getInBufferSize(),
ff,
sink
);
// originalTxt needs to be a static text and not a mutable sink because it's referred to in template nodes
alertTemplate.parse(Chars.toString(sink), now, properties);
}
if (alertTemplate.getKeyOffset(MESSAGE_ENV) < 0) {
throw new LogError(String.format(
"Bad template, no %s declaration found %s",
MESSAGE_ENV_VALUE,
location));
}
alertTemplateNodes = alertTemplate.getTemplateNodes();
alertTemplateNodesLen = alertTemplateNodes.size();
}
static {
if (!ALERT_PROPS.contains(ORG_ID_ENV)) {
ALERT_PROPS.put(ORG_ID_ENV, DEFAULT_ENV_VALUE);
}
if (!ALERT_PROPS.contains(NAMESPACE_ENV)) {
ALERT_PROPS.put(NAMESPACE_ENV, DEFAULT_ENV_VALUE);
}
if (!ALERT_PROPS.contains(CLUSTER_ENV)) {
ALERT_PROPS.put(CLUSTER_ENV, DEFAULT_ENV_VALUE);
}
if (!ALERT_PROPS.contains(INSTANCE_ENV)) {
ALERT_PROPS.put(INSTANCE_ENV, DEFAULT_ENV_VALUE);
}
ALERT_PROPS.put(MESSAGE_ENV, MESSAGE_ENV_VALUE);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy