001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.metrics;
020
021import com.sun.net.httpserver.HttpServer;
022import org.jdom2.Element;
023import org.jpos.core.ConfigurationException;
024import org.jpos.core.annotation.Config;
025import org.jpos.q2.QBeanSupport;
026
027import java.io.IOException;
028import java.io.OutputStream;
029import java.net.InetSocketAddress;
030
031/**
032 * QBean that exposes the Q2 Prometheus meter registry over an embedded HTTP
033 * server, optionally serving a {@code status-path} probe used by orchestrators.
034 */
035public class PrometheusService extends QBeanSupport {
036    /** Default constructor; no instance state to initialise. */
037    public PrometheusService() {}
038    @Config("port")
039    private int port;
040    @Config("path")
041    private String path;
042    @Config("status-path")
043    private String statusPath;
044    private HttpServer server;
045
046    @Override
047    protected void initService() throws ConfigurationException {
048        try {
049            final var registry =  getServer().getPrometheusMeterRegistry();
050            server = HttpServer.create(new InetSocketAddress(port), 0);
051            server.createContext(path, httpExchange -> {
052                String response = registry.scrape();
053                httpExchange.getResponseHeaders().add("Content-Type", "text/plain; version=0.0.4");
054                httpExchange.sendResponseHeaders(200, response.getBytes().length);
055                try (OutputStream os = httpExchange.getResponseBody()) {
056                    os.write(response.getBytes());
057                }
058            });
059            if (statusPath != null) {
060                server.createContext(statusPath, httpExchange -> {
061                    String response = getServer().running() ? "running\n" : "stopping\n";
062                    httpExchange.getResponseHeaders().add("Content-Type", "text/plain");
063                    httpExchange.sendResponseHeaders(200, response.getBytes().length);
064                    try (OutputStream os = httpExchange.getResponseBody()) {
065                        os.write(response.getBytes());
066                    }
067                });
068            }
069            Thread.ofVirtual().start(server::start);
070        } catch (IOException e) {
071            getLog().warn(e);
072            throw new RuntimeException(e);
073        }
074    }
075
076    @Override
077    protected void stopService() {
078        server.stop(2);
079    }
080
081    /**
082     * Builds a {@code <prometheus>} bean descriptor with the given configuration.
083     *
084     * @param port HTTP port to bind
085     * @param path path that serves the Prometheus scrape
086     * @param statusPath path that serves the status probe, or {@code null} to disable
087     * @return populated descriptor element
088     */
089    public static Element createDescriptor (int port, String path, String statusPath) {
090        return new Element("prometheus")
091          .addContent(createProperty ("port", Integer.toString(port)))
092          .addContent (createProperty ("path", path))
093          .addContent (createProperty ("status-path", statusPath));
094
095    }
096    private static Element createProperty (String name, String value) {
097        return new Element ("property")
098          .setAttribute("name", name)
099          .setAttribute("value", value);
100    }
101}