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 io.micrometer.core.instrument.Meter;
022import io.micrometer.core.instrument.MeterRegistry;
023import io.micrometer.core.instrument.Tags;
024import io.micrometer.core.instrument.Timer;
025import io.micrometer.core.instrument.Counter;
026import io.micrometer.core.instrument.Gauge;
027import io.micrometer.core.instrument.search.Search;
028
029import java.time.Duration;
030import java.util.Arrays;
031import java.util.Objects;
032import java.util.concurrent.Callable;
033import java.util.concurrent.locks.Lock;
034import java.util.concurrent.locks.ReentrantLock;
035import java.util.function.Supplier;
036
037/**
038 * Factory helpers that create or look up Micrometer meters defined by
039 * {@link MeterInfo}, ensuring duplicate registrations resolve to the same instance.
040 */
041public class MeterFactory {
042    /** Default constructor; no instance state to initialise. */
043    public MeterFactory() {}
044    private static final Lock metersLock = new ReentrantLock();
045
046    /**
047     * Returns the {@link Timer} associated with {@code meterInfo} and {@code tags},
048     * creating it (with histogram and 50/95 percentiles) when absent.
049     *
050     * @param registry the Micrometer registry
051     * @param meterInfo meter id/description/default-tag descriptor
052     * @param tags extra tags to combine with {@link MeterInfo#add(Tags)}
053     * @return the (possibly existing) Timer
054     */
055    public static Timer timer(MeterRegistry registry, MeterInfo meterInfo, Tags tags) {
056        return createMeter(registry, meterInfo, tags,
057          () -> Timer.builder(meterInfo.id()).tags(meterInfo.add(tags)).description(meterInfo.description())
058            .publishPercentiles(0.5, 0.95)
059            .publishPercentileHistogram()
060            .minimumExpectedValue(Duration.ofMillis(1))
061            .maximumExpectedValue(Duration.ofSeconds(60))
062            .register(registry));
063    }
064
065    /**
066     * Returns the {@link Counter} associated with {@code meterInfo} and {@code tags},
067     * creating it when absent.
068     *
069     * @param registry the Micrometer registry
070     * @param meterInfo meter id/description/default-tag descriptor
071     * @param tags extra tags to combine with {@link MeterInfo#add(Tags)}
072     * @return the (possibly existing) Counter
073     */
074    public static Counter counter(MeterRegistry registry, MeterInfo meterInfo, Tags tags) {
075        return createMeter(registry, meterInfo, tags,
076          () -> Counter.builder(meterInfo.id()).tags(meterInfo.add(tags)).description(meterInfo.description()).register(registry));
077    }
078
079    /**
080     * Registers (or updates) a freely-named {@link Counter}, bypassing the
081     * {@link MeterInfo} catalog.
082     *
083     * @param registry the Micrometer registry
084     * @param meterName meter id
085     * @param tags meter tags
086     * @param description meter description
087     * @return the registered Counter
088     */
089    public static Counter updateCounter(MeterRegistry registry, String meterName, Tags tags, String description) {
090        return Counter.builder(meterName).tags(tags).description(description).register(registry);
091    }
092
093    /**
094     * Registers (or updates) the {@link Counter} identified by {@code meterInfo}.
095     *
096     * @param registry the Micrometer registry
097     * @param meterInfo meter id/description/default-tag descriptor
098     * @param tags extra tags to combine with {@link MeterInfo#add(Tags)}
099     * @return the registered Counter
100     */
101    public static Counter updateCounter(MeterRegistry registry, MeterInfo meterInfo, Tags tags) {
102        return updateCounter(registry, meterInfo.id(), meterInfo.add(tags), meterInfo.description());
103    }
104
105    /**
106     * Returns the {@link Gauge} associated with {@code meterInfo} and {@code tags},
107     * creating one bound to {@code n} when absent.
108     *
109     * @param registry the Micrometer registry
110     * @param meterInfo meter id/description/default-tag descriptor
111     * @param tags extra tags to combine with {@link MeterInfo#add(Tags)}
112     * @param unit base unit, or {@code null} for none
113     * @param n supplier called to read the current gauge value
114     * @return the (possibly existing) Gauge
115     */
116    public static Gauge gauge(MeterRegistry registry, MeterInfo meterInfo, Tags tags, String unit, Supplier<Number> n) {
117        return createMeter(registry, meterInfo, tags,
118          () -> Gauge.builder(meterInfo.id(), n)
119            .tags(meterInfo.add(tags))
120            .description(meterInfo.description())
121            .baseUnit(unit)
122            .register(registry));
123    }
124
125    /**
126     * Removes the supplied meters from the registry, skipping {@code null} entries.
127     *
128     * @param registry the Micrometer registry
129     * @param meters meters to remove
130     */
131    public static void remove  (MeterRegistry registry, Meter... meters) {
132        Arrays.stream(meters).filter(Objects::nonNull).forEach(registry::remove);
133    }
134
135    @SuppressWarnings("unchecked")
136    private static <T extends Meter> T createMeter(MeterRegistry registry, MeterInfo meterInfo, Tags tags, Callable<T> creator) {
137        metersLock.lock();
138        try {
139            T meter = (T) Search.in(registry).name(meterInfo.id()).tags(meterInfo.add(tags)).meter();
140            if (meter == null) {
141                try {
142                    meter = creator.call();
143                } catch (Exception e) {
144                    throw new RuntimeException (e);
145                }
146            }
147            return meter;
148        } finally {
149            metersLock.unlock();
150        }
151    }
152}