blob: 214d78c5332d4113e440b76077bb7b2a6dae81d6 [file] [log] [blame]
/*
* 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.sentry.api.service.thrift;
import com.codahale.metrics.ConsoleReporter;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.MetricSet;
import com.codahale.metrics.Slf4jReporter;
import com.codahale.metrics.Timer;
import com.codahale.metrics.json.MetricsModule;
import com.codahale.metrics.jvm.BufferPoolMetricSet;
import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.hadoop.conf.Configuration;
import org.apache.sentry.provider.db.service.persistent.SentryStore;
import org.apache.sentry.provider.db.service.persistent.SentryStoreInterface;
import org.apache.sentry.service.thrift.SentryService;
import org.apache.sentry.api.common.SentryServiceUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.codahale.metrics.MetricRegistry.name;
import static org.apache.sentry.api.service.thrift.SentryMetricsServletContextListener.METRIC_REGISTRY;
import static org.apache.sentry.service.common.ServiceConstants.ServerConfig;
/**
* A singleton class which holds metrics related utility functions as well as the list of metrics.
*/
public final class SentryMetrics {
public enum Reporting {
JMX,
CONSOLE,
LOG,
JSON,
}
private static final Logger LOGGER = LoggerFactory
.getLogger(SentryMetrics.class);
private static SentryMetrics sentryMetrics = null;
private final AtomicBoolean reportingInitialized = new AtomicBoolean();
private boolean gaugesAdded = false;
private boolean sentryServiceGaugesAdded = false;
final Timer createRoleTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "create-role"));
final Timer dropRoleTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "drop-role"));
final Timer grantRoleTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "grant-role"));
final Timer revokeRoleTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "revoke-role"));
final Timer grantTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "grant-privilege"));
final Timer revokeTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "revoke-privilege"));
final Timer dropPrivilegeTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "drop-privilege"));
final Timer renamePrivilegeTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "rename-privilege"));
final Timer listRolesByGroupTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-roles-by-group"));
final Timer listPrivilegesByRoleTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-privileges-by-role"));
final Timer listPrivilegesByUserTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-privileges-by-user"));
final Timer listPrivilegesForUserTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-sentry-privileges-by-user-and-itsgroups"));
final Timer listPrivilegesForProviderTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-privileges-for-provider"));
final Timer listPrivilegesByAuthorizableTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-privileges-by-authorizable"));
final Timer listRolesPrivilegesTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-roles-privileges"));
final Timer listUsersPrivilegesTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "list-users-privileges"));
final Timer notificationProcessTimer = METRIC_REGISTRY.timer(
name(SentryPolicyStoreProcessor.class, "process-hsm-notification"));
/**
* Return a Timer with name.
*/
public Timer getTimer(String name) {
return METRIC_REGISTRY.timer(name);
}
/**
* Return a Histogram with name.
*/
public Histogram getHistogram(String name) {
return METRIC_REGISTRY.histogram(name);
}
/**
* Return a Counter with name.
*/
public Counter getCounter(String name) {
return METRIC_REGISTRY.counter(name);
}
private SentryMetrics() {
registerMetricSet("gc", new GarbageCollectorMetricSet(), METRIC_REGISTRY);
registerMetricSet("buffers",
new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()),
METRIC_REGISTRY);
registerMetricSet("memory", new MemoryUsageGaugeSet(), METRIC_REGISTRY);
registerMetricSet("threads", new ThreadStatesGaugeSet(), METRIC_REGISTRY);
}
/**
* Get singleton instance.
*/
public static synchronized SentryMetrics getInstance() {
if (sentryMetrics == null) {
sentryMetrics = new SentryMetrics();
}
return sentryMetrics;
}
void addSentryStoreGauges(SentryStoreInterface sentryStore) {
if (!gaugesAdded) {
addGauge(SentryStore.class, "role_count", sentryStore.getRoleCountGauge());
addGauge(SentryStore.class, "privilege_count",
sentryStore.getPrivilegeCountGauge());
addGauge(SentryStore.class, "group_count", sentryStore.getGroupCountGauge());
addGauge(SentryStore.class, "hms.waiters", sentryStore.getHMSWaitersCountGauge());
addGauge(SentryStore.class, "hms.notification.id",
sentryStore.getLastNotificationIdGauge());
addGauge(SentryStore.class, "hms.snapshot.paths.id",
sentryStore.getLastPathsSnapshotIdGauge());
addGauge(SentryStore.class, "hms.perm.change.id",
sentryStore.getPermChangeIdGauge());
addGauge(SentryStore.class, "hms.path.change.id",
sentryStore.getPathChangeIdGauge());
addGauge(SentryStore.class, "hms.authz_objects_count",
sentryStore.getAuthzObjectsCountGauge());
addGauge(SentryStore.class, "hms.authz_paths_count",
sentryStore.getAuthzPathsCountGauge());
gaugesAdded = true;
}
}
/**
* Add gauges for the SentryService class.
* @param sentryservice
*/
public void addSentryServiceGauges(SentryService sentryservice) {
if (!sentryServiceGaugesAdded) {
addGauge(SentryService.class, "is_active", sentryservice.getIsActiveGauge());
addGauge(SentryService.class, "activated", sentryservice.getBecomeActiveCount());
sentryServiceGaugesAdded = true;
}
}
/**
* Initialize reporters. Only initializes once.<p>
*
* Available reporters:
* <ul>
* <li>console</li>
* <li>log</li>
* <li>jmx</li>
* </ul>
*
* <p><For console reporter configre it to report every
* <em>SENTRY_REPORTER_INTERVAL_SEC</em> seconds.
*
* <p>Method is thread safe.
*/
@SuppressWarnings("squid:S2095")
void initReporting(Configuration conf) {
final String reporter = conf.get(ServerConfig.SENTRY_REPORTER);
if ((reporter == null) || reporter.isEmpty() || reportingInitialized.getAndSet(true)) {
// Nothing to do, just return
return;
}
final int reportInterval =
conf.getInt(ServerConfig.SENTRY_REPORTER_INTERVAL_SEC,
ServerConfig.SENTRY_REPORTER_INTERVAL_DEFAULT);
// Get list of configured reporters
Set<String> reporters = new HashSet<>();
for (String r: reporter.split(",")) {
reporters.add(r.trim().toUpperCase());
}
// In case there are no reporters, configure JSON reporter
if (reporters.isEmpty()) {
reporters.add(Reporting.JSON.toString());
}
// Configure all reporters
for (String r: reporters) {
switch (SentryMetrics.Reporting.valueOf(r)) {
case CONSOLE:
LOGGER.info("Enabled console metrics reporter with {} seconds interval",
reportInterval);
final ConsoleReporter consoleReporter =
ConsoleReporter.forRegistry(METRIC_REGISTRY)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build();
consoleReporter.start(reportInterval, TimeUnit.SECONDS);
break;
case JMX:
LOGGER.info("Enabled JMX metrics reporter");
final JmxReporter jmxReporter = JmxReporter.forRegistry(METRIC_REGISTRY)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build();
jmxReporter.start();
break;
case LOG:
LOGGER.info("Enabled Log4J metrics reporter with {} seconds interval",
reportInterval);
final Slf4jReporter logReporter = Slf4jReporter.forRegistry(METRIC_REGISTRY)
.outputTo(LOGGER)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build();
logReporter.start(reportInterval, TimeUnit.SECONDS);
break;
case JSON:
LOGGER.info("Enabled JSON metrics reporter with {} seconds interval", reportInterval);
JsonFileReporter jsonReporter = new JsonFileReporter(conf,
reportInterval, TimeUnit.SECONDS);
jsonReporter.start();
break;
default:
LOGGER.warn("Invalid metrics reporter {}", reporter);
break;
}
}
}
private <T, V> void addGauge(Class<T> tClass, String gaugeName, Gauge<V> gauge) {
METRIC_REGISTRY.register(
name(tClass, gaugeName), gauge);
}
private void registerMetricSet(String prefix, MetricSet metricSet, MetricRegistry registry) {
for (Map.Entry<String, Metric> entry : metricSet.getMetrics().entrySet()) {
if (entry.getValue() instanceof MetricSet) {
registerMetricSet(prefix + "." + entry.getKey(), (MetricSet) entry.getValue(), registry);
} else {
registry.register(prefix + "." + entry.getKey(), entry.getValue());
}
}
}
/**
* Custom reporter that writes metrics as a JSON file.
* This class originated from Apache Hive JSON reporter.
*/
private static class JsonFileReporter implements AutoCloseable, Runnable {
//
// Implementation notes.
//
// 1. Since only local file systems are supported, there is no need to use Hadoop
// version of Path class.
// 2. java.nio package provides modern implementation of file and directory operations
// which is better then the traditional java.io, so we are using it here.
// In particular, it supports atomic creation of temporary files with specified
// permissions in the specified directory. This also avoids various attacks possible
// when temp file name is generated first, followed by file creation.
// See http://www.oracle.com/technetwork/articles/javase/nio-139333.html for
// the description of NIO API and
// http://docs.oracle.com/javase/tutorial/essential/io/legacy.html for the
// description of interoperability between legacy IO api vs NIO API.
// 3. To avoid race conditions with readers of the metrics file, the implementation
// dumps metrics to a temporary file in the same directory as the actual metrics
// file and then renames it to the destination. Since both are located on the same
// filesystem, this rename is likely to be atomic (as long as the underlying OS
// support atomic renames.
//
// Permissions for the metrics file
private static final FileAttribute<Set<PosixFilePermission>> FILE_ATTRS =
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-r--r--"));
private static final String JSON_REPORTER_THREAD_NAME = "json-reporter";
private ScheduledExecutorService executor = null;
private final ObjectMapper jsonMapper =
new ObjectMapper().registerModule(new MetricsModule(TimeUnit.SECONDS,
TimeUnit.MILLISECONDS,
false));
private final Configuration conf;
/** Destination file name. */
// Location of JSON file
private final Path path;
// tmpdir is the dirname(path)
private final Path tmpDir;
private final long interval;
private final TimeUnit unit;
JsonFileReporter(Configuration conf, long interval, TimeUnit unit) {
this.conf = conf;
String pathString = conf.get(ServerConfig.SENTRY_JSON_REPORTER_FILE,
ServerConfig.SENTRY_JSON_REPORTER_FILE_DEFAULT);
path = Paths.get(pathString).toAbsolutePath();
LOGGER.info("Reporting metrics to {}", path);
// We want to use tmpDir i the same directory as the destination file to support atomic
// move of temp file to the destination metrics file
tmpDir = path.getParent();
this.interval = interval;
this.unit = unit;
}
private void start() {
executor = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder().setNameFormat(JSON_REPORTER_THREAD_NAME).build());
executor.scheduleAtFixedRate(this, 0, interval, unit);
}
@Override
public void run() {
Path tmpFile = null;
try {
String json = null;
try {
json = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(METRIC_REGISTRY);
} catch (JsonProcessingException e) {
LOGGER.error("Error converting metrics to JSON", e);
return;
}
// Metrics are first dumped to a temp file which is then renamed to the destination
try {
tmpFile = Files.createTempFile(tmpDir, "smetrics", "json", FILE_ATTRS);
} catch (IOException e) {
LOGGER.error("failed to create temp file for JSON metrics", e);
return;
} catch (SecurityException e) {
// This shouldn't ever happen
LOGGER.error("failed to create temp file for JSON metrics: no permissions", e);
return;
} catch (UnsupportedOperationException e) {
// This shouldn't ever happen
LOGGER.error("failed to create temp file for JSON metrics: operartion not supported", e);
return;
}
try (BufferedWriter bw = new BufferedWriter(new FileWriter(tmpFile.toFile()))) {
bw.write(json);
}
// Move temp file to the destination file
try {
Files.move(tmpFile, path, StandardCopyOption.ATOMIC_MOVE);
} catch (Exception e) {
LOGGER.error("Failed to move temp metrics file to {}: {}", path, e.getMessage());
}
} catch (Throwable t) {
// catch all errors (throwable and execptions to prevent subsequent tasks from being suppressed)
LOGGER.error("Error executing scheduled task ", t);
} finally {
// If something happened and we were not able to rename the temp file, attempt to remove it
if (tmpFile != null && tmpFile.toFile().exists()) {
// Attempt to delete temp file, if this fails, not much can be done about it.
try {
Files.delete(tmpFile);
} catch (Exception e) {
LOGGER.error("failed to delete yemporary metrics file {}", tmpFile, e);
}
}
}
}
@Override
public void close() {
if (executor != null) {
SentryServiceUtil.shutdownAndAwaitTermination(executor,
JSON_REPORTER_THREAD_NAME, 1, TimeUnit.MINUTES, LOGGER);
executor = null;
}
try {
Files.delete(path);
} catch (IOException e) {
LOGGER.error("Unable to delete {}", path, e);
}
}
}
}