001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.core.net;
015
016import ch.qos.logback.core.util.CloseUtil;
017
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.OutputStream;
021import java.net.DatagramPacket;
022import java.net.DatagramSocket;
023import java.net.InetAddress;
024import java.net.InetSocketAddress;
025import java.net.PortUnreachableException;
026import java.net.SocketException;
027import java.net.UnknownHostException;
028
029/**
030 * SyslogOutputStream is a wrapper around the {@link DatagramSocket} class so
031 * that it behaves like an {@link OutputStream}.
032 */
033public class SyslogOutputStream extends OutputStream {
034
035    /**
036     * The maximum length after which we discard the existing string buffer and
037     * start anew.
038     */
039    private static final int MAX_LEN = 1024;
040
041    private InetAddress address;
042    private DatagramSocket ds;
043    private ByteArrayOutputStream baos = new ByteArrayOutputStream();
044    final private int port;
045    private SocketException connectException;
046
047    public SyslogOutputStream(String syslogHost, int port) throws UnknownHostException, SocketException {
048        this.address = InetAddress.getByName(syslogHost);
049        this.port = port;
050        this.ds = new DatagramSocket();
051        // This stream only sends datagrams to the syslog host. Connecting the socket removes it from
052        // the list of unconnected (wildcard) UDP listeners reported by tools like `ss -ulnp`.
053        // See https://github.com/qos-ch/logback/issues/939
054        try {
055            this.ds.connect(new InetSocketAddress(address, port));
056        } catch (SocketException e) {
057            // connecting may fail, e.g. if the network is not yet available. In that case
058            // fall back to an unconnected socket, i.e. the behavior prior to issue 939.
059            this.connectException = e;
060        }
061    }
062
063    boolean isValidState() {
064        if(ds == null || baos == null || address == null) {
065            return false;
066        }
067        return true;
068    }
069
070    boolean isConnected() {
071        return ds != null && ds.isConnected();
072    }
073
074    /**
075     * Returns the exception thrown when connecting the underlying socket to the
076     * syslog host, or null if the connection succeeded.
077     */
078    SocketException getConnectException() {
079        return connectException;
080    }
081
082    public void write(byte[] byteArray, int offset, int len) throws IOException {
083        if(!isValidState()) return;
084        baos.write(byteArray, offset, len);
085    }
086
087    public void flush() throws IOException {
088        if(!isValidState()) return;
089        byte[] bytes = baos.toByteArray();
090        DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, port);
091
092        // clean up for next round
093        if (baos.size() > MAX_LEN) {
094            baos = new ByteArrayOutputStream();
095        } else {
096            baos.reset();
097        }
098
099        // after a failure, it can happen that bytes.length is zero
100        // in that case, there is no point in sending out an empty message/
101        if (bytes.length == 0) {
102            return;
103        }
104        if (this.ds != null) {
105            try {
106                ds.send(packet);
107            } catch (PortUnreachableException e) {
108                // On a connected socket, an ICMP port unreachable message triggered by a
109                // previously sent datagram is reported on the next send, in which case the
110                // current packet is not sent. Retry once so that the current packet is not lost,
111                // as was the case with unconnected sockets. Should the retry fail as well, the
112                // exception propagates to the caller.
113                ds.send(packet);
114            }
115        }
116
117    }
118
119    @Override
120    public void close() {
121        CloseUtil.closeQuietly(ds);
122        CloseUtil.closeQuietly(baos);
123        address = null;
124        ds = null;
125        baos = null;
126    }
127
128    public int getPort() {
129        return port;
130    }
131
132    @Override
133    public void write(int b) throws IOException {
134        baos.write(b);
135    }
136
137    int getSendBufferSize() throws SocketException {
138        return ds.getSendBufferSize();
139    }
140}