/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.solr.metrics.reporters.solr;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.apache.solr.cloud.CloudDescriptor;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.core.SolrCore;
import org.apache.solr.handler.admin.MetricsCollectorHandler;
import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.metrics.SolrMetricReporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class reports selected metrics from replicas to shard leader.
* <p>The following configuration properties are supported:</p>
* <ul>
* <li>handler - (optional str) handler path where reports are sent. Default is
* {@link MetricsCollectorHandler#HANDLER_PATH}.</li>
* <li>period - (optional int) how often reports are sent, in seconds. Default is 60. Setting this
* to 0 disables the reporter.</li>
* <li>filter - (optional multiple str) regex expression(s) matching selected metrics to be reported.</li>
* </ul>
* NOTE: this reporter uses predefined "replica" group, and it's always created even if explicit configuration
* is missing. Default configuration uses filters defined in {@link #DEFAULT_FILTERS}.
* <p>Example configuration:</p>
* <pre>
* <reporter name="test" group="replica">
* <int name="period">11</int>
* <str name="filter">UPDATE\./update/.*requests</str>
* <str name="filter">QUERY\./select.*requests</str>
* </reporter>
* </pre>
*/
public class SolrShardReporter extends SolrMetricReporter {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final List<String> DEFAULT_FILTERS = new ArrayList(){{
add("TLOG.*");
add("CORE\\.fs.*");
add("REPLICATION.*");
add("INDEX\\.flush.*");
add("INDEX\\.merge\\.major.*");
add("UPDATE\\./update/.*requests");
add("QUERY\\./select.*requests");
}};
private String handler = MetricsCollectorHandler.HANDLER_PATH;
private int period = SolrMetricManager.DEFAULT_CLOUD_REPORTER_PERIOD;
private List<String> filters = new ArrayList<>();
private SolrReporter reporter;
/**
* Create a reporter for metrics managed in a named registry.
*
* @param metricManager metric manager
* @param registryName registry to use, one of registries managed by
* {@link SolrMetricManager}
*/
public SolrShardReporter(SolrMetricManager metricManager, String registryName) {
super(metricManager, registryName);
}
public void setHandler(String handler) {
this.handler = handler;
}
public void setPeriod(int period) {
this.period = period;
}
public void setFilter(List<String> filterConfig) {
if (filterConfig == null || filterConfig.isEmpty()) {
return;
}
filters.addAll(filterConfig);
}
public void setFilter(String filter) {
if (filter != null && !filter.isEmpty()) {
this.filters.add(filter);
}
}
// for unit tests
int getPeriod() {
return period;
}
@Override
protected void validate() throws IllegalStateException {
if (filters.isEmpty()) {
filters = DEFAULT_FILTERS;
}
// start in inform(...) only when core is available
}
@Override
public void close() throws IOException {
if (reporter != null) {
reporter.close();
}
}
public void setCore(SolrCore core) {
if (reporter != null) {
reporter.close();
}
if (!enabled) {
log.info("Reporter disabled for registry " + registryName);
return;
}
if (core.getCoreDescriptor().getCloudDescriptor() == null) {
// not a cloud core
log.warn("Not initializing shard reporter for non-cloud core " + core.getName());
return;
}
if (period < 1) { // don't start it
log.warn("period=" + period + ", not starting shard reporter ");
return;
}
// our id is coreNodeName
String id = core.getCoreDescriptor().getCloudDescriptor().getCoreNodeName();
// target registry is the leaderRegistryName
String groupId = core.getCoreMetricManager().getLeaderRegistryName();
if (groupId == null) {
log.warn("No leaderRegistryName for core " + core + ", not starting the reporter...");
return;
}
SolrReporter.Report spec = new SolrReporter.Report(groupId, null, registryName, filters);
reporter = SolrReporter.Builder.forReports(metricManager, Collections.singletonList(spec))
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.withHandler(handler)
.withReporterId(id)
.setCompact(true)
.cloudClient(false) // we want to send reports specifically to a selected leader instance
.skipAggregateValues(true) // we don't want to transport details of aggregates
.skipHistograms(true) // we don't want to transport histograms
.build(core.getCoreContainer().getUpdateShardHandler().getHttpClient(), new LeaderUrlSupplier(core));
reporter.start(period, TimeUnit.SECONDS);
}
private static class LeaderUrlSupplier implements Supplier<String> {
private SolrCore core;
LeaderUrlSupplier(SolrCore core) {
this.core = core;
}
@Override
public String get() {
CloudDescriptor cd = core.getCoreDescriptor().getCloudDescriptor();
if (cd == null) {
return null;
}
ClusterState state = core.getCoreContainer().getZkController().getClusterState();
DocCollection collection = state.getCollection(core.getCoreDescriptor().getCollectionName());
Replica replica = collection.getLeader(core.getCoreDescriptor().getCloudDescriptor().getShardId());
if (replica == null) {
log.warn("No leader for " + collection.getName() + "/" + core.getCoreDescriptor().getCloudDescriptor().getShardId());
return null;
}
String baseUrl = replica.getStr("base_url");
if (baseUrl == null) {
log.warn("No base_url for replica " + replica);
}
return baseUrl;
}
}
}