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}