LogbackのJMX設定インタフェースを使って設定ファイルの切り替えを行う
LogbackにはJMXで設定を行える機能がついていて、設定ファイルで
<configuration> <jmxConfigurator /> ...
と jmxConfigurator 要素を追加するだけで有効にできる。あとは、jconsole等を使えば外部から設定のリロードや設定ファイルの切り替えを行うことができる。
問題なのが、どこからjconsoleを使ってアクセスするかで、対象アプリケーションの起動時に -Dcom.sun.management.jmxremote.port=ポート番号 などと指定すればリモートからも設定ができるようになるけれども認証の設定が面倒だし(OSの認証と統合できないのでID管理やら何やらの手間が増える。認証なしは論外)、対象マシンにログインしてローカルで動かせばいいじゃんという案も「jconsoleは重いので実運用でローカル環境から使用するのはお勧めしない」とか聞いてしまうと躊躇してしまう。
なので、
- 切り替えは対象のOS上にログインして行う
- jconsoleは使わず、コマンドラインツールで切り替える
という運用を考える。
あとは、「コマンドラインで切り替える」ための適当なツールが無いので試作。(tools.jarとargs4jとLogbackが必要)
- 引数なしで実行
- ローカルの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="引数文字列"