LogbackのJMX設定インタフェースを使って設定ファイルの切り替えを行う

LogbackにはJMXで設定を行える機能がついていて、設定ファイルで

<configuration>

  <jmxConfigurator />

  ...

と jmxConfigurator 要素を追加するだけで有効にできる。あとは、jconsole等を使えば外部から設定のリロードや設定ファイルの切り替えを行うことができる。

問題なのが、どこからjconsoleを使ってアクセスするかで、対象アプリケーションの起動時に -Dcom.sun.management.jmxremote.port=ポート番号 などと指定すればリモートからも設定ができるようになるけれども認証の設定が面倒だし(OSの認証と統合できないのでID管理やら何やらの手間が増える。認証なしは論外)、対象マシンにログインしてローカルで動かせばいいじゃんという案も「jconsoleは重いので実運用でローカル環境から使用するのはお勧めしない」とか聞いてしまうと躊躇してしまう。

なので、

  • 切り替えは対象のOS上にログインして行う
  • jconsoleは使わず、コマンドラインツールで切り替える

という運用を考える。

あとは、「コマンドラインで切り替える」ための適当なツールが無いので試作。(tools.jarとargs4jLogbackが必要)

引数なしで実行
ローカルのJVMの一覧を出力
-vm <vmid>
指定のVM上で動いているLogbackのデフォルト設定をリロード
-vm <vmid> -url <url>
指定のVM上で動いているLogbackの設定をurlで指定したファイルに切り替え
-vm <vmid> -contextName <contextName>
LogbackのcontextNameを指定(デフォルトのdefaultから変更している場合)
package example.logback;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Set;

import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;

import ch.qos.logback.classic.jmx.JMXConfigurator;
import ch.qos.logback.classic.jmx.JMXConfiguratorMBean;
import ch.qos.logback.classic.jmx.MBeanUtil;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class LogbackConfigSwitcher
{
    static final String LOCAL_CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";

    @Option(name="-vm", usage="Virtual Machine ID")
    public String vmid;

    @Option(name="-contextName", usage="Logback ContextName")
    public String contextName = "default";

    @Option(name="-url", usage="Logback Configuration URL")
    public URL url;

    public void run() {
        out("Listing VMs ...");
        List<VirtualMachineDescriptor> vmds = VirtualMachine.list();

        if (vmid == null) {
            out("********** Local VMs **********");
            for (VirtualMachineDescriptor vmd : vmds) {
                out("%s: %s%n", vmd.id(), vmd.displayName());
            }
            return;
        }

        out("Searching for the VM [%s] ...", vmid);
        for (VirtualMachineDescriptor vmd : vmds) {
            if (vmid.equals(vmd.id())) {
                out("Found the VM.");

                try {
                    MBeanServerConnection connection = connectToVM(vmd);
                    JMXConfiguratorMBean mbean =
                        getLogbackConfiguratorMBean(connection, contextName);
                    if (mbean == null) {
                        return;
                    }

                    if (url == null) {
                        mbean.reloadDefaultConfiguration();
                        out("SUCCESS: reloadDefaultConfiguration()");
                    } else {
                        mbean.reloadByURL(url);
                        out("SUCCESS: reloadByURL(\"%s\")", url);
                    }
                } catch (Exception e) {
                    err(e.getMessage());
                    err(e);
                }

                return;
            }
        }

        err("vmid [%s] is not found.", vmid);
    }

    /**
     * @see http://java.sun.com/javase/ja/6/docs/ja/technotes/guides/management/agent.html
     */
    private MBeanServerConnection connectToVM(VirtualMachineDescriptor vmd)
            throws AttachNotSupportedException, IOException,
            AgentLoadException, AgentInitializationException {

        VirtualMachine vm = VirtualMachine.attach(vmd);
        out("Attached to the VM [%s].", vmd);

        String address = (String) vm.getAgentProperties().get(LOCAL_CONNECTOR_ADDRESS);

        if (address == null) {
            err("JMX agent has not been loaded. Loading management-agent.jar ...");
            String agent = vm.getSystemProperties().getProperty("java.home") +
                File.separator + "lib" + File.separator + "management-agent.jar";
           vm.loadAgent(agent);

           address = (String) vm.getAgentProperties().get(LOCAL_CONNECTOR_ADDRESS);
        }

        vm.detach();

        out("Connecting to [%s]", address);
        JMXServiceURL url = new JMXServiceURL(address);
        JMXConnector connector = JMXConnectorFactory.connect(url);
        out("Connected.");

        return connector.getMBeanServerConnection();
    }

    /**
     * @see ch.qos.logback.classic.joran.action.JMXConfiguratorAction
     */
    private JMXConfiguratorMBean getLogbackConfiguratorMBean(
            MBeanServerConnection connection, String logbackContextName)
    throws MalformedObjectNameException, IOException {

        String objectName = MBeanUtil.getObjectNameFor(
                logbackContextName, JMXConfigurator.class);
        out("Querying object [%s]", objectName);
        Set<ObjectName> objectNames = connection.queryNames(new ObjectName(objectName), null);

        if (objectNames.size() != 1) {
            err("ObjectName set size is [%d]", objectNames.size());
            return null;
        }
        out("Found object.", objectName);

        return MBeanServerInvocationHandler.newProxyInstance(
                connection, objectNames.iterator().next(), JMXConfiguratorMBean.class, false);
    }

    private void out(String format, Object ... args) {
        System.out.printf(format + "%n", args);
    }

    private void err(String format, Object ... args) {
        System.err.printf(format + "%n", args);
    }

    private void err(Throwable t) {
        t.printStackTrace(System.err);
    }

    public static void main(String[] args) throws Exception
    {
        LogbackConfigSwitcher app = new LogbackConfigSwitcher();
        CmdLineParser parser = new CmdLineParser(app);
        try {
            parser.parseArgument(args);
        } catch (CmdLineException e) {
            System.err.println(e.getMessage());
            parser.printUsage(System.err);
        }
        app.run();
    }
}


jconsoleよりは軽いだろうけど、どのくらいリソースを食うかは不明。

作る課程で知ったこと

  • Logbackのjmxconfigurator要素にはマニュアルにも無い? objectNameという属性が指定できる。 objectName="LOGBACK:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator" とか指定するとそういうobjectNameでMBeanとして登録される。
  • Java6からは対象アプリの起動時に -Dcom.sun.management.jmxremote しなくても、上のようにして後からロードさせることが可能。
  • Mavenコマンドラインアプリを実行するには mvn exec:java -Dexec.mainClass=<mainのあるクラスの完全修飾名> -Dexec.args="引数文字列"