All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ru.yandex.clickhouse.jdbcbridge.JdbcBridgeVerticle Maven / Gradle / Ivy

/**
 * Copyright 2019-2021, Zhichun Wu
 *
 * 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 ru.yandex.clickhouse.jdbcbridge;

import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.core.instrument.binder.system.UptimeMetrics;
import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.ResponseContentTypeHandler;
import io.vertx.ext.web.handler.TimeoutHandler;
import io.vertx.micrometer.MicrometerMetricsOptions;
import io.vertx.micrometer.PrometheusScrapingHandler;
import io.vertx.micrometer.VertxPrometheusOptions;
import ru.yandex.clickhouse.jdbcbridge.core.ByteBuffer;
import ru.yandex.clickhouse.jdbcbridge.core.ColumnDefinition;
import ru.yandex.clickhouse.jdbcbridge.core.TableDefinition;
import ru.yandex.clickhouse.jdbcbridge.core.DataType;
import ru.yandex.clickhouse.jdbcbridge.core.Extension;
import ru.yandex.clickhouse.jdbcbridge.core.ExtensionManager;
import ru.yandex.clickhouse.jdbcbridge.core.NamedDataSource;
import ru.yandex.clickhouse.jdbcbridge.core.NamedQuery;
import ru.yandex.clickhouse.jdbcbridge.core.NamedSchema;
import ru.yandex.clickhouse.jdbcbridge.core.QueryParameters;
import ru.yandex.clickhouse.jdbcbridge.core.QueryParser;
import ru.yandex.clickhouse.jdbcbridge.core.Repository;
import ru.yandex.clickhouse.jdbcbridge.core.RepositoryManager;
import ru.yandex.clickhouse.jdbcbridge.core.ResponseWriter;
import ru.yandex.clickhouse.jdbcbridge.core.Utils;
import ru.yandex.clickhouse.jdbcbridge.impl.ConfigDataSource;
import ru.yandex.clickhouse.jdbcbridge.impl.JdbcDataSource;
import ru.yandex.clickhouse.jdbcbridge.impl.JsonFileRepository;
import ru.yandex.clickhouse.jdbcbridge.impl.ScriptDataSource;

import static ru.yandex.clickhouse.jdbcbridge.core.DataType.*;

/**
 * Unified data source bridge for ClickHouse.
 *
 * @since 2.0
 */
public class JdbcBridgeVerticle extends AbstractVerticle implements ExtensionManager {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JdbcBridgeVerticle.class);

    private static volatile long startTime;

    private static final String CONFIG_PATH = Utils.getConfiguration("config", "CONFIG_DIR", "jdbc-bridge.config.dir");
    private static final boolean SERIAL_MODE = Boolean
            .valueOf(Utils.getConfiguration("false", "SERIAL_MODE", "jdbc-bridge.serial.mode"));

    private static final int DEFAULT_SERVER_PORT = 9019;

    private static final String RESPONSE_CONTENT_TYPE = "application/octet-stream";

    private static final String WRITE_RESPONSE = "Ok.";
    private static final String PING_RESPONSE = WRITE_RESPONSE + "\n";
    private static final String SCHEMA_ALLOWED_RESPONSE = "1\n";

    private final List> extensions;

    private final RepositoryManager repos;

    private long scanInterval = 5000L;

    List> loadRepositories(JsonObject serverConfig) {
        List> repos = new ArrayList<>();

        Extension defaultExt = new Extension<>(JsonFileRepository.class);
        JsonArray declaredRepos = serverConfig == null ? null : serverConfig.getJsonArray("repositories");
        if (declaredRepos == null) {
            // let's go with default extensions
            declaredRepos = new JsonArray();

            declaredRepos.add(NamedDataSource.class.getName());
            declaredRepos.add(NamedSchema.class.getName());
            declaredRepos.add(NamedQuery.class.getName());
        }

        for (Object item : declaredRepos) {
            Repository repo = null;

            if (item instanceof String) {
                repo = defaultExt.newInstance(this, defaultExt.loadClass(String.valueOf(item)));
            } else {
                JsonObject o = (JsonObject) item;

                String entityClassName = o.getString("entity");
                if (entityClassName == null || entityClassName.isEmpty()) {
                    continue;
                }

                String repoClassName = o.getString("repository");

                ArrayList urls = null;
                JsonArray libUrls = o.getJsonArray("libUrls");
                if (repoClassName != null && !repoClassName.isEmpty() && libUrls != null) {
                    urls = new ArrayList<>(libUrls.size());
                    for (Object u : libUrls) {
                        if (u instanceof String) {
                            urls.add((String) u);
                        }
                    }
                }

                Extension ext = Utils.loadExtension(urls, repoClassName);
                if (ext != null) {
                    repo = (Repository) ext.newInstance(this, ext.loadClass(entityClassName));
                }
            }

            if (repo != null) {
                repos.add(repo);
            }
        }

        return repos;
    }

    List> loadExtensions(JsonObject serverConfig) {
        List> exts = new ArrayList<>();

        JsonArray declaredExts = serverConfig == null ? null : serverConfig.getJsonArray("extensions");
        if (declaredExts == null) {
            // let's go with default extensions
            declaredExts = new JsonArray();

            declaredExts.add(JdbcDataSource.class.getName());
            declaredExts.add(ConfigDataSource.class.getName());
            declaredExts.add(ScriptDataSource.class.getName());
        }

        for (Object item : declaredExts) {
            Extension ext = null;

            if (item instanceof String) {
                ext = Utils.loadExtension((String) item);
            } else {
                JsonObject o = (JsonObject) item;

                String className = o.getString("class");

                JsonArray libUrls = o.getJsonArray("libUrls");
                if (libUrls != null) {
                    ArrayList urls = new ArrayList<>(libUrls.size());
                    for (Object u : libUrls) {
                        if (u instanceof String) {
                            urls.add((String) u);
                        }
                    }

                    // FIXME duplicated extensions?
                    ext = Utils.loadExtension(urls, className);
                } else {
                    ext = Utils.loadExtension(className);
                }
            }

            if (ext != null) {
                exts.add(ext);
            }
        }

        return exts;
    }

    public JdbcBridgeVerticle() {
        super();

        this.extensions = new ArrayList<>();

        this.repos = Utils.loadService(RepositoryManager.class);
    }

    @Override
    public void start() {
        JsonObject config = Utils.loadJsonFromFile(Paths
                .get(CONFIG_PATH,
                        Utils.getConfiguration("server.json", "SERVER_CONFIG_FILE", "jdbc-bridge.server.config.file"))
                .toString());

        this.scanInterval = config.getLong("configScanPeriod", 5000L);

        this.repos.update(this.loadRepositories(config));
        // extension must be loaded *after* repository is initialized
        this.extensions.addAll(this.loadExtensions(config));

        // initialize extensions so that they're fully ready for use
        for (Extension ext : this.extensions) {
            ext.initialize(this);
        }

        startServer(config,
                Utils.loadJsonFromFile(Paths.get(CONFIG_PATH,
                        Utils.getConfiguration("httpd.json", "HTTPD_CONFIG_FILE", "jdbc-bridge.httpd.config.file"))
                        .toString()));
    }

    private void startServer(JsonObject bridgeServerConfig, JsonObject httpServerConfig) {
        if (httpServerConfig.isEmpty()) {
            // make sure we can pass long query/script by default
            httpServerConfig.put("maxInitialLineLength", 2147483647L);
        }

        HttpServer server = vertx.createHttpServer(new HttpServerOptions(httpServerConfig));
        // vertx.createHttpServer(new
        // HttpServerOptions().setTcpNoDelay(false).setTcpKeepAlive(true)
        // .setTcpFastOpen(true).setLogActivity(true));

        long requestTimeout = bridgeServerConfig.getLong("requestTimeout", 5000L);
        long queryTimeout = Math.max(requestTimeout, bridgeServerConfig.getLong("queryTimeout", 60000L));

        TimeoutHandler requestTimeoutHandler = TimeoutHandler.create(requestTimeout);
        TimeoutHandler queryTimeoutHandler = TimeoutHandler.create(queryTimeout);

        // https://github.com/vert-x3/vertx-examples/blob/master/web-examples/src/main/java/io/vertx/example/web/mongo/Server.java
        Router router = Router.router(vertx);

        router.route("/metrics").handler(PrometheusScrapingHandler.create());

        router.route().handler(BodyHandler.create()).handler(this::responseHandlers)
                .handler(ResponseContentTypeHandler.create()).failureHandler(this::errorHandler);

        // stateless endpoints
        router.get("/ping").handler(requestTimeoutHandler).handler(this::handlePing);
        router.get("/schema_allowed").handler(requestTimeoutHandler).handler(this::handleSchemaAllowed);

        router.post("/identifier_quote").produces(RESPONSE_CONTENT_TYPE).handler(requestTimeoutHandler)
                .handler(this::handleIdentifierQuote);
        router.post("/columns_info").produces(RESPONSE_CONTENT_TYPE).handler(queryTimeoutHandler)
                .handler(this::handleColumnsInfo);
        router.post("/").produces(RESPONSE_CONTENT_TYPE).handler(queryTimeoutHandler).blockingHandler(this::handleQuery,
                SERIAL_MODE);
        router.post("/write").produces(RESPONSE_CONTENT_TYPE).handler(queryTimeoutHandler)
                .blockingHandler(this::handleWrite, SERIAL_MODE);

        log.info("Starting web server...");
        int port = bridgeServerConfig.getInteger("serverPort", DEFAULT_SERVER_PORT);
        server.requestHandler(router).listen(port, action -> {
            if (action.succeeded()) {
                log.info("Server http://0.0.0.0:{} started in {} ms", port, System.currentTimeMillis() - startTime);
            } else {
                log.error("Failed to start server", action.cause());
            }
        });
    }

    private void responseHandlers(RoutingContext ctx) {
        HttpServerRequest req = ctx.request();

        String path = ctx.normalisedPath();
        if (log.isDebugEnabled()) {
            log.debug("[{}] Context:\n{}", path, ctx.data());
            log.debug("[{}] Headers:\n{}", path, req.headers());
            log.debug("[{}] Parameters:\n{}", path, req.params());
        }

        // bad assumption here as it may lead to UTF8 decode issue like #83
        // if (log.isTraceEnabled()) {
        // log.trace("[{}] Body:\n{}", path, ctx.getBodyAsString());
        // }

        ctx.response().endHandler(handler -> {
            if (log.isTraceEnabled()) {
                log.trace("[{}] About to end response...", ctx.normalisedPath());
            }
        }).closeHandler(handler -> {
            if (log.isTraceEnabled()) {
                log.trace("[{}] About to close response...", ctx.normalisedPath());
            }
        }).drainHandler(handler -> {
            if (log.isTraceEnabled()) {
                log.trace("[{}] About to drain response...", ctx.normalisedPath());
            }
        }).exceptionHandler(throwable -> {
            log.error("Caught exception", throwable);
        });

        ctx.next();
    }

    private void errorHandler(RoutingContext ctx) {
        log.error("Failed to respond", ctx.failure());
        ctx.response().setStatusCode(500).end(ctx.failure().getMessage());
    }

    private void handlePing(RoutingContext ctx) {
        ctx.response().end(PING_RESPONSE);
    }

    private void handleSchemaAllowed(RoutingContext ctx) {
        // TODO some datasources do not support schema
        ctx.response().end(SCHEMA_ALLOWED_RESPONSE);
    }

    private NamedDataSource getDataSource(String uri, boolean orCreate) {
        return getDataSource(getDataSourceRepository(), uri, orCreate);
    }

    private NamedDataSource getDataSource(Repository repo, String uri, boolean orCreate) {
        NamedDataSource ds = repo.get(uri);

        return ds == null && orCreate ? new NamedDataSource(uri, null, null) : ds;
    }

    private void handleColumnsInfo(RoutingContext ctx) {
        final QueryParser parser = QueryParser.fromRequest(ctx, getDataSourceRepository());

        // priority: named/inline schema -> named query -> type inferring
        TableDefinition tableDef = null;

        // check if we got named schema first
        String rawSchema = parser.getRawSchema();
        NamedSchema namedSchema = getSchemaRepository().get(rawSchema);
        if (namedSchema == null) { // try harder as we may got an inline schema
            String schema = parser.getNormalizedSchema();
            if (schema.indexOf(' ') != -1) {
                if (log.isDebugEnabled()) {
                    log.debug("Got inline schema:\n[{}]", schema);
                }
                tableDef = TableDefinition.fromString(schema);
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Got named schema:\n[{}]", namedSchema);
            }
            tableDef = namedSchema.getColumns();
        }

        String rawQuery = parser.getRawQuery();
        log.info("Raw query:\n{}", rawQuery);

        String uri = parser.getConnectionString();

        QueryParameters params = parser.getQueryParameters();
        NamedDataSource ds = getDataSource(uri, params.isDebug());
        String dsId = uri;
        if (ds != null) {
            dsId = ds.getId();
            params = ds.newQueryParameters(params);
        }

        if (tableDef == null) {
            // even it's a named query, the column list could be empty
            NamedQuery namedQuery = getQueryRepository().get(rawQuery);

            if (namedQuery != null) {
                if (namedSchema == null) {
                    namedSchema = getSchemaRepository().get(namedQuery.getSchema());
                }

                tableDef = namedSchema != null ? namedSchema.getColumns() : namedQuery.getColumns();
            } else {
                tableDef = namedSchema != null ? namedSchema.getColumns()
                        : ds.getResultColumns(rawSchema, parser.getNormalizedQuery(), params);
            }
        }

        List additionalColumns = new ArrayList();
        if (params.showDatasourceColumn()) {
            additionalColumns.add(new ColumnDefinition(TableDefinition.COLUMN_DATASOURCE, DataType.Str, true,
                    DEFAULT_LENGTH, DEFAULT_PRECISION, DEFAULT_SCALE, null, dsId, null));
        }
        if (params.showCustomColumns() && ds != null) {
            additionalColumns.addAll(ds.getCustomColumns());
        }

        if (additionalColumns.size() > 0) {
            tableDef = new TableDefinition(tableDef, true,
                    additionalColumns.toArray(new ColumnDefinition[additionalColumns.size()]));
        }

        String columnsInfo = tableDef.toString();

        if (log.isDebugEnabled()) {
            log.debug("Columns info:\n[{}]", columnsInfo);
        }
        ctx.response().end(ByteBuffer.asBuffer(columnsInfo));
    }

    private void handleIdentifierQuote(RoutingContext ctx) {
        // don't want to repeat datasource lookup here
        ctx.response().end(ByteBuffer.asBuffer(NamedDataSource.DEFAULT_QUOTE_IDENTIFIER));
    }

    private void handleQuery(RoutingContext ctx) {
        final Repository manager = getDataSourceRepository();
        final QueryParser parser = QueryParser.fromRequest(ctx, manager);

        final HttpServerResponse resp = ctx.response().setChunked(true);

        if (log.isTraceEnabled()) {
            log.trace("About to execute query...");
        }

        QueryParameters params = parser.getQueryParameters();
        NamedDataSource ds = getDataSource(manager, parser.getConnectionString(), params.isDebug());
        params = ds.newQueryParameters(params);

        String rawSchema = parser.getRawSchema();
        NamedSchema namedSchema = getSchemaRepository().get(rawSchema);

        String generatedQuery = parser.getRawQuery();
        String normalizedQuery = parser.getNormalizedQuery();
        // try if it's a named query first
        NamedQuery namedQuery = getQueryRepository().get(normalizedQuery);
        // in case the "query" is a local file...
        normalizedQuery = ds.loadSavedQueryAsNeeded(normalizedQuery, params);

        if (log.isDebugEnabled()) {
            log.debug("Generated query:\n{}\nNormalized query:\n{}", generatedQuery, normalizedQuery);
        }

        ResponseWriter writer = new ResponseWriter(resp, parser.getStreamOptions(),
                ds.getQueryTimeout(params.getTimeout()));

        long executionStartTime = System.currentTimeMillis();
        if (namedQuery != null) {
            if (log.isDebugEnabled()) {
                log.debug("Found named query: [{}]", namedQuery);
            }

            if (namedSchema == null) {
                namedSchema = getSchemaRepository().get(namedQuery.getSchema());
            }
            // columns in request might just be a subset of defined list
            // for example:
            // - named query 'test' is: select a, b, c from table
            // - clickhouse query: select b, a from jdbc('?','','test')
            // - requested columns: b, a
            ds.executeQuery(rawSchema, namedQuery, namedSchema != null ? namedSchema.getColumns() : parser.getTable(),
                    params, writer);
        } else {
            // columnsInfo could be different from what we responded earlier, so let's parse
            // it again
            TableDefinition queryColumns = namedSchema != null ? namedSchema.getColumns() : parser.getTable();
            // unfortunately default values will be lost between two requests, so we have to
            // add it back...
            List additionalColumns = new ArrayList();
            if (params.showDatasourceColumn()) {
                additionalColumns.add(new ColumnDefinition(TableDefinition.COLUMN_DATASOURCE, DataType.Str, true,
                        DEFAULT_LENGTH, DEFAULT_PRECISION, DEFAULT_SCALE, null, ds.getId(), null));
            }
            if (params.showCustomColumns()) {
                additionalColumns.addAll(ds.getCustomColumns());
            }

            queryColumns.updateValues(additionalColumns);

            ds.executeQuery(namedSchema == null ? rawSchema : Utils.EMPTY_STRING, parser.getNormalizedQuery(),
                    normalizedQuery, queryColumns, params, writer);
        }

        if (log.isDebugEnabled()) {
            log.debug("Completed execution in {} ms.", System.currentTimeMillis() - executionStartTime);
        }

        resp.end();
    }

    // https://github.com/ClickHouse/ClickHouse/blob/bee5849c6a7dba20dbd24dfc5fd5a786745d90ff/programs/odbc-bridge/MainHandler.cpp#L169
    private void handleWrite(RoutingContext ctx) {
        final Repository manager = getDataSourceRepository();
        final QueryParser parser = QueryParser.fromRequest(ctx, manager, true);

        final HttpServerResponse resp = ctx.response().setChunked(true);

        if (log.isTraceEnabled()) {
            log.trace("About to execute mutation...");
        }

        QueryParameters params = parser.getQueryParameters();
        NamedDataSource ds = getDataSource(manager, parser.getConnectionString(), params.isDebug());
        params = ds == null ? params : ds.newQueryParameters(params);

        final String generatedQuery = parser.getRawQuery();

        String normalizedQuery = parser.getNormalizedQuery();
        if (log.isDebugEnabled()) {
            log.debug("Generated query:\n{}\nNormalized query:\n{}", generatedQuery, normalizedQuery);
        }

        // try if it's a named query first
        NamedQuery namedQuery = getQueryRepository().get(normalizedQuery);
        // in case the "query" is a local file...
        normalizedQuery = ds.loadSavedQueryAsNeeded(normalizedQuery, params);

        // TODO: use named schema as table name?

        String table = parser.getRawQuery();
        if (namedQuery != null) {
            table = parser.extractTable(ds.loadSavedQueryAsNeeded(namedQuery.getQuery(), params));
        } else {
            table = parser.extractTable(ds.loadSavedQueryAsNeeded(normalizedQuery, params));
        }

        ResponseWriter writer = new ResponseWriter(resp, parser.getStreamOptions(),
                ds.getWriteTimeout(params.getTimeout()));

        ds.executeMutation(parser.getRawSchema(), table, parser.getTable(), params, ByteBuffer.wrap(ctx.getBody()),
                writer);

        if (writer.isOpen()) {
            resp.end(ByteBuffer.asBuffer(WRITE_RESPONSE));
        }
    }

    private Repository getDataSourceRepository() {
        return getRepositoryManager().getRepository(NamedDataSource.class);
    }

    private Repository getSchemaRepository() {
        return getRepositoryManager().getRepository(NamedSchema.class);
    }

    private Repository getQueryRepository() {
        return getRepositoryManager().getRepository(NamedQuery.class);
    }

    @SuppressWarnings("unchecked")
    @Override
    public  Extension getExtension(Class clazz) {
        String className = Objects.requireNonNull(clazz).getName();

        Extension ext = null;

        for (Extension e : this.extensions) {
            if (e.getProviderClass().getName().equals(className)) {
                ext = (Extension) e;
            }
        }

        return ext;
    }

    @Override
    public RepositoryManager getRepositoryManager() {
        return this.repos;
    }

    @Override
    public void registerConfigLoader(String configPath, Consumer consumer) {
        final String path = Paths.get(CONFIG_PATH, configPath).toString();

        log.info("Start to monitor configuration file(s) at [{}]", path);

        ConfigRetriever retriever = ConfigRetriever.create(vertx,
                new ConfigRetrieverOptions().setScanPeriod(this.scanInterval)
                        .addStore(new ConfigStoreOptions().setType("directory")
                                .setConfig(new JsonObject().put("path", path).put("filesets", new JsonArray()
                                        .add(new JsonObject().put("pattern", "*.json").put("format", "json"))))));

        retriever.getConfig(action -> {
            if (action.succeeded()) {
                vertx.executeBlocking(promise -> {
                    consumer.accept(action.result());
                }, true, res -> {
                    if (!res.succeeded()) {
                        log.error("Failed to load configuration", res.cause());
                    }
                });
            } else {
                log.warn("Not able to load configuration from [{}] due to {}", path, action.cause().getMessage());
            }
        });

        retriever.listen(change -> {
            log.info("Configuration change in [{}] detected", path);

            vertx.executeBlocking(promise -> {
                consumer.accept(change.getNewConfiguration());
            }, true, res -> {
                if (!res.succeeded()) {
                    log.error("Failed to reload configuration", res.cause());
                }
            });
        });
    }

    @Override
    public Map getScriptableObjects() {
        Map vars = new HashMap<>();

        // TODO and some utilities?
        vars.put("__vertx", vertx);
        vars.put("__datasources", getDataSourceRepository());
        vars.put("__schemas", getSchemaRepository());
        vars.put("__queries", getQueryRepository());

        return vars;
    }

    public static void main(String[] args) {
        startTime = System.currentTimeMillis();

        VertxOptions options = new VertxOptions(Utils.loadJsonFromFile(Paths
                .get(CONFIG_PATH,
                        Utils.getConfiguration("vertx.json", "VERTX_CONFIG_FILE", "jdbc-bridge.vertx.config.file"))
                .toString()));

        Object metricRegistry = Utils.getDefaultMetricRegistry();
        // only MicroMeter is supported at this point
        if (metricRegistry instanceof MeterRegistry) {
            MeterRegistry registry = (MeterRegistry) metricRegistry;
            new ClassLoaderMetrics().bindTo(registry);
            new JvmGcMetrics().bindTo(registry);
            new JvmMemoryMetrics().bindTo(registry);
            new JvmThreadMetrics().bindTo(registry);
            new ProcessorMetrics().bindTo(registry);
            new UptimeMetrics().bindTo(registry);

            options.setMetricsOptions(
                    new MicrometerMetricsOptions().setPrometheusOptions(new VertxPrometheusOptions().setEnabled(true))
                            .setMicrometerRegistry(registry).setEnabled(true));
        }

        // https://github.com/eclipse-vertx/vert.x/blob/master/src/main/generated/io/vertx/core/VertxOptionsConverter.java
        Vertx vertx = Vertx.vertx(options);

        vertx.deployVerticle(new JdbcBridgeVerticle());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy