/** * Copyright (C) 2009-2015 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.service.statusmonitor; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.Arrays; import java.util.List; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.foundationdb.Database; import com.foundationdb.Transaction; import com.foundationdb.ais.model.TableName; import com.foundationdb.async.Function; import com.foundationdb.async.Future; import com.foundationdb.directory.DirectoryLayer; import com.foundationdb.directory.DirectorySubspace; import com.foundationdb.qp.operator.RowCursor; import com.foundationdb.qp.row.Row; import com.foundationdb.server.api.dml.ColumnSelector; import com.foundationdb.server.service.Service; import com.foundationdb.server.service.config.ConfigurationService; import com.foundationdb.server.service.externaldata.GenericRowTracker; import com.foundationdb.server.service.externaldata.JsonRowWriter; import com.foundationdb.server.store.FDBHolder; import com.foundationdb.server.types.FormatOptions; import com.foundationdb.server.types.value.ValueSource; import com.foundationdb.sql.Main; import com.foundationdb.sql.embedded.EmbeddedJDBCService; import com.foundationdb.sql.embedded.JDBCDriver; import com.foundationdb.sql.embedded.JDBCResultSet; import com.foundationdb.sql.embedded.JDBCResultSetMetaData; import com.foundationdb.tuple.Tuple2; import com.foundationdb.util.AkibanAppender; import com.foundationdb.util.JsonUtils; import com.google.inject.Inject; public class StatusMonitorServiceImpl implements StatusMonitorService, Service { private final ConfigurationService configService; private final FDBHolder fdbService; private final EmbeddedJDBCService jdbcService; private static final Logger logger = LoggerFactory.getLogger(StatusMonitorServiceImpl.class); public static final List<String> STATUS_MONITOR_DIR = Arrays.asList("Status Monitor","Layers"); public static final String STATUS_MONITOR_LAYER_NAME = "SQL Layer"; public static final String CONFIG_STATUS_ENABLE = "fdbsql.fdb.status.enabled"; protected byte[] instanceKey; private volatile boolean running; private Future<Void> instanceWatch; FormatOptions options; @Inject public StatusMonitorServiceImpl (ConfigurationService configService, FDBHolder fdbService, EmbeddedJDBCService jdbcService) { this.configService= configService; this.fdbService = fdbService; this.jdbcService = jdbcService; this.options = new FormatOptions(); } @Override public void start() { // If not enabled (e.g. during testing), turn off the service. if (!Boolean.parseBoolean(configService.getProperty(CONFIG_STATUS_ENABLE))) { return; } options.set(FormatOptions.JsonBinaryFormatOption.fromProperty(configService.getProperty("fdbsql.sql.jsonbinary_output"))); DirectorySubspace rootDirectory = DirectoryLayer.getDefault().createOrOpen(fdbService.getTransactionContext(), STATUS_MONITOR_DIR).get(); instanceKey = rootDirectory.pack(Tuple2.from(STATUS_MONITOR_LAYER_NAME, configService.getInstanceID())); running = true; writeStatus(); } @Override public void stop() { running = false; // Could/should clear instanceKey but writing in stop() isn't possible due to shutdown hook. clearWatch(); } @Override public void crash() { stop(); } protected Database getDatabase() { return fdbService.getDatabase(); } private void writeStatus () { logger.debug("Writing status"); clearWatch(); String status = generateStatus(); final byte[] jsonData = Tuple2.from(status).pack(); getDatabase() .run(new Function<Transaction,Void>() { @Override public Void apply(Transaction tr) { tr.options().setPrioritySystemImmediate(); tr.set (instanceKey, jsonData); setWatch(tr); return null; } }); } private void setWatch(Transaction tr) { logger.debug("Setting watch"); // Initiate a watch (from this same transaction) for changes to the key // used to signal configuration changes. instanceWatch = tr.watch(instanceKey); instanceWatch.onReady(new Runnable() { @Override public void run() { logger.debug("Watch fired"); if(running) { writeStatus(); } } }); } private void clearWatch() { if (instanceWatch != null) { logger.debug("Clearing watch"); instanceWatch.cancel(); instanceWatch = null; } } private String generateStatus() { StringWriter str = new StringWriter(); try { JsonGenerator gen = JsonUtils.createJsonGenerator(str); gen.writeStartObject(); gen.writeStringField("id", configService.getInstanceID()); gen.writeStringField("name", STATUS_MONITOR_LAYER_NAME); gen.writeNumberField("timestamp", System.currentTimeMillis()); gen.writeStringField("version", Main.VERSION_INFO.versionLong); // TODO: Set transaction as priority immediate when possible Properties props = new Properties(); props.setProperty("database", TableName.INFORMATION_SCHEMA); try (Connection conn = jdbcService.getDriver().connect(JDBCDriver.URL, props); Statement s = conn.createStatement()) { summary(s, INSTANCE, INSTANCE_SQL, gen, false); summary(s, SERVERS, SERVERS_SQL, gen, true); summary(s, SESSIONS, SESSIONS_SQL, gen, true); summary(s, STATISTICS, STATISTICS_SQL, gen, false); summary(s, GARBAGE_COLLECTORS, GARBAGE_COLLECTORS_SQL, gen, true); summary(s, MEMORY_POOLS, MEMORY_POOLS_SQL, gen, true); } gen.writeEndObject(); gen.flush(); } catch (SQLException | IOException ex) { logger.error("Unable to generate status", ex); return null; } if (logger.isTraceEnabled()) { logger.trace("status: {}", str.toString()); } return str.toString(); } private static final String INSTANCE = "instance"; private static final String INSTANCE_SQL = "select server_id as id, server_host as host, "+ "server_store as store, server_jit_compiler_time as jit_compiler_time from information_schema.server_instance_summary"; private static final String SERVERS = "servers"; private static final String SERVERS_SQL = "select server_type, local_port, unix_timestamp(start_time) as start_time, session_count from information_schema.server_servers"; private static final String SESSIONS = "sessions"; private static final String SESSIONS_SQL = "select session_id, unix_timestamp(start_time) as start_time, server_type, remote_address,"+ "query_count, failed_query_count, query_from_cache, logged_statements," + "call_statement_count, ddl_statement_count, dml_statement_count, select_statement_count," + "other_statement_count from information_schema.server_sessions "+ // exclude our own session "WHERE session_id <> CURRENT_SESSION_ID()"; private static final String STATISTICS = "statistics"; private static final String STATISTICS_SQL = "select * from information_schema.server_statistics_summary"; private static final String GARBAGE_COLLECTORS = "garbage_collectors"; private static final String GARBAGE_COLLECTORS_SQL = "select * from information_schema.server_garbage_collectors"; private static final String MEMORY_POOLS = "memory_pools"; private static final String MEMORY_POOLS_SQL = "select * from information_schema.server_memory_pools"; protected void summary (Statement s, String name, String sql, JsonGenerator gen, boolean arrayWrapper) throws IOException, SQLException { logger.trace("summary: {}", name); if (arrayWrapper) { gen.writeArrayFieldStart(name); } else { gen.writeFieldName(name); } JDBCResultSet rs = (JDBCResultSet)s.executeQuery(sql); StringWriter strings = new StringWriter(); PrintWriter writer = new PrintWriter(strings); collectResults(rs, writer, options); gen.writeRawValue(strings.toString()); if (arrayWrapper) { gen.writeEndArray(); } } private void collectResults(JDBCResultSet resultSet, PrintWriter writer, FormatOptions opt) throws SQLException { AkibanAppender appender = AkibanAppender.of(writer); SQLOutput cursor = new SQLOutput(resultSet); try { JsonRowWriter jsonRowWriter = new JsonRowWriter(cursor); jsonRowWriter.writeRowsFromOpenCursor(cursor, appender, "", cursor, opt); } finally { cursor.close(); } } private class SQLOutput extends GenericRowTracker implements RowCursor, JsonRowWriter.WriteRow { private JDBCResultSet resultSet; public SQLOutput (JDBCResultSet rs) { this.resultSet = rs; } @Override public void open() { } @Override public void close() { } @Override public Row next() { try { if (resultSet.next()) { return resultSet.unwrap(Row.class); } else { return null; } } catch(SQLException e) { throw new IllegalStateException(e); } } @Override public void jump(Row row, ColumnSelector columnSelector) { throw new UnsupportedOperationException(); } @Override public boolean isIdle() { throw new UnsupportedOperationException(); } @Override public boolean isActive() { throw new UnsupportedOperationException(); } @Override public boolean isClosed() { throw new UnsupportedOperationException(); } @Override public void setIdle() { } @Override public String getRowName() { return null; } @Override public void write(Row row, AkibanAppender appender, FormatOptions options) { try { JDBCResultSetMetaData metaData = resultSet.getMetaData(); boolean begun = false; for(int col = 1; col <= metaData.getColumnCount(); ++col) { String colName = metaData.getColumnLabel(col); ValueSource valueSource = row.value(col - 1); JsonRowWriter.writeValue(colName, valueSource, appender, !begun, options); begun = true; } } catch(SQLException e) { throw new IllegalStateException(e); } } } }