Skip to content

Commit

Permalink
[Java] Dynamic logging (aeron-io#1167)
Browse files Browse the repository at this point in the history
* [Git] Exclude `.run` directory where Intellij stores run configs.

* [Java] Dynamically attach logging agent to a running Java process + add support for starting and stopping logging.

* [Java] Translate exception into user-friendly message + print information about which log reader is used and file the log goes to.

* [Java] Do not pass file name or log reader class name, i.e. all config options are non-mandatory.

* [Java] Spelling.

* [Java] Make `aeron-all` depend on `aeron-agent` module so that it can access Agent classes.

* [Bash] Add a script to start and stop dynamic logging.
Usage example:
```
JVM_OPTS="-Daeron.event.log=admin -Daeron.event.archive.log=all" ./dynamic-logging 1111 start
```

* [Bash] Fix number of args check.

* [Batch] Add a script for dynamic logging.

* [Java] Show cause for the failed agent command.

* [Java] Restore public API.

* [Java] Enums should throw IllegalArgumentException if ID is unknown to avoid NPE when looked up code is to be used.
  • Loading branch information
vyazelenko authored Apr 17, 2021
1 parent 9e5f11a commit a2f7df5
Show file tree
Hide file tree
Showing 19 changed files with 937 additions and 167 deletions.
11 changes: 6 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ GTAGS
GRTAGS
GPATH
prop
out
classes
bin
out/
classes/
bin/

# OS X
.DS_Store
Expand All @@ -19,7 +19,8 @@ bin
*.iml
*.ipr
*.iws
.idea
.idea/
.run/

# editors
*.sublime-project
Expand All @@ -28,7 +29,7 @@ bin

# the build
build-local.properties
.gradle
.gradle/
build*
cmake-build-debug/
cmake-build-release/
Expand Down
13 changes: 12 additions & 1 deletion aeron-agent/src/main/java/io/aeron/agent/ArchiveEventCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,18 @@ private static ArchiveEventCode[] createLookupArray(

static ArchiveEventCode get(final int id)
{
return id >= 0 && id < EVENT_CODE_BY_ID.length ? EVENT_CODE_BY_ID[id] : null;
if (id < 0 || id >= EVENT_CODE_BY_ID.length)
{
throw new IllegalArgumentException("no ArchiveEventCode for id: " + id);
}

final ArchiveEventCode code = EVENT_CODE_BY_ID[id];
if (null == code)
{
throw new IllegalArgumentException("no ArchiveEventCode for id: " + id);
}

return code;
}

static ArchiveEventCode getByTemplateId(final int templateId)
Expand Down
13 changes: 12 additions & 1 deletion aeron-agent/src/main/java/io/aeron/agent/ClusterEventCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,18 @@ public enum ClusterEventCode implements EventCode

static ClusterEventCode get(final int id)
{
return id >= 0 && id < EVENT_CODE_BY_ID.length ? EVENT_CODE_BY_ID[id] : null;
if (id < 0 || id >= EVENT_CODE_BY_ID.length)
{
throw new IllegalArgumentException("no ClusterEventCode for id: " + id);
}

final ClusterEventCode code = EVENT_CODE_BY_ID[id];
if (null == code)
{
throw new IllegalArgumentException("no ClusterEventCode for id: " + id);
}

return code;
}

/**
Expand Down
184 changes: 184 additions & 0 deletions aeron-agent/src/main/java/io/aeron/agent/ConfigOption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright 2014-2021 Real Logic Limited.
*
* Licensed 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
*
* https://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 io.aeron.agent;

import org.agrona.Strings;
import org.agrona.concurrent.Agent;

import java.util.EnumMap;
import java.util.Map;

/**
* A set of configuration options.
*/
enum ConfigOption
{
/**
* Event Buffer log file name system property. If not set then output will default to {@link System#out}.
*/
LOG_FILENAME("aeron.event.log.filename"),

/**
* Event reader {@link Agent} which consumes the {@link EventConfiguration#EVENT_RING_BUFFER} to output log events.
*/
READER_CLASSNAME("aeron.event.log.reader.classname"),

/**
* Driver Event tags system property. This is either:
* <ul>
* <li>A comma separated list of {@link DriverEventCode}s to enable</li>
* <li>"all" which enables all the codes</li>
* <li>"admin" which enables the codes specified by {@link EventConfiguration#ADMIN_ONLY_EVENT_CODES} which
* is the admin commands</li>
* </ul>
*/
ENABLED_DRIVER_EVENT_CODES("aeron.event.log"),

/**
* Disabled Driver Event tags system property. Follows the format specified for
* {@link #ENABLED_DRIVER_EVENT_CODES}. This property will disable any codes in the set
* specified there. Defined on its own has no effect.
*/
DISABLED_DRIVER_EVENT_CODES("aeron.event.log.disable"),

/**
* Archive Event tags system property. This is either:
* <ul>
* <li>A comma separated list of {@link ArchiveEventCode}s to enable</li>
* <li>"all" which enables all the codes</li>
* </ul>
*/
ENABLED_ARCHIVE_EVENT_CODES("aeron.event.archive.log"),

/**
* Disabled Archive Event tags system property. Follows the format specified for
* {@link #ENABLED_ARCHIVE_EVENT_CODES}. This property will disable any codes in the
* set specified there. Defined on its own has no effect.
*/
DISABLED_ARCHIVE_EVENT_CODES("aeron.event.archive.log.disable"),

/**
* Cluster Event tags system property. This is either:
* <ul>
* <li>A comma separated list of {@link ClusterEventCode}s to enable</li>
* <li>"all" which enables all the codes</li>
* </ul>
*/
ENABLED_CLUSTER_EVENT_CODES("aeron.event.cluster.log"),

/**
* Disabled Cluster Event tags system property name. Follows the format specified for
* {@link #ENABLED_CLUSTER_EVENT_CODES}. This property will disable any codes in the
* set specified there. Defined on its own has no effect.
*/
DISABLED_CLUSTER_EVENT_CODES("aeron.event.cluster.log.disable");

static final String START_COMMAND = "start";
static final String STOP_COMMAND = "stop";

private static final char VALUE_SEPARATOR = '=';
private static final char OPTION_SEPARATOR = '|';
private static final ConfigOption[] OPTIONS = values();

private final String propertyName;

ConfigOption(final String propertyName)
{
this.propertyName = propertyName;
}

String propertyName()
{
return propertyName;
}

static EnumMap<ConfigOption, String> fromSystemProperties()
{
final EnumMap<ConfigOption, String> values = new EnumMap<>(ConfigOption.class);
for (final ConfigOption option : OPTIONS)
{
final String value = System.getProperty(option.propertyName());
if (null != value)
{
values.put(option, value);
}
}
return values;
}

static String buildAgentArgs(final EnumMap<ConfigOption, String> configOptions)
{
if (configOptions.isEmpty())
{
return null;
}

final StringBuilder builder = new StringBuilder();
for (final Map.Entry<ConfigOption, String> entry : configOptions.entrySet())
{
builder.append(entry.getKey())
.append(VALUE_SEPARATOR)
.append(entry.getValue())
.append(OPTION_SEPARATOR);
}
if (builder.length() > 0)
{
builder.deleteCharAt(builder.length() - 1);
}
return builder.toString();
}

static EnumMap<ConfigOption, String> parseAgentArgs(final String agentArgs)
{
if (Strings.isEmpty(agentArgs))
{
throw new IllegalArgumentException("cannot parse empty value");
}

final EnumMap<ConfigOption, String> values = new EnumMap<>(ConfigOption.class);

int optionIndex = -1;
do
{
final int valueIndex = agentArgs.indexOf(VALUE_SEPARATOR, optionIndex);
if (valueIndex <= 0)
{
break;
}

int nameIndex = -1;
while (optionIndex < valueIndex)
{
nameIndex = optionIndex;
optionIndex = agentArgs.indexOf(OPTION_SEPARATOR, optionIndex + 1);
if (optionIndex < 0)
{
break;
}
}

final String optionName = agentArgs.substring(nameIndex + 1, valueIndex);
final ConfigOption option = valueOf(optionName);
final String value = agentArgs.substring(
valueIndex + 1,
optionIndex > 0 ? optionIndex : agentArgs.length());
values.put(option, value);
}
while (optionIndex > 0);

return values;
}
}
123 changes: 123 additions & 0 deletions aeron-agent/src/main/java/io/aeron/agent/DynamicLoggingAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2014-2021 Real Logic Limited.
*
* Licensed 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
*
* https://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 io.aeron.agent;

import net.bytebuddy.agent.ByteBuddyAgent;
import org.agrona.PropertyAction;
import org.agrona.Strings;
import org.agrona.SystemUtil;

import java.io.File;
import java.nio.file.Paths;
import java.util.EnumMap;

import static io.aeron.agent.ConfigOption.*;
import static java.lang.System.out;

/**
* Attach/detach logging agent dynamically to a running process.
*/
public class DynamicLoggingAgent
{
/**
* Attach logging agent jar to a running process.
*
* @param args program arguments.
*/
public static void main(final String[] args)
{
if (args.length < 3)
{
printHelp();
System.exit(-1);
}

final File agentJar = Paths.get(args[0]).toAbsolutePath().toFile();
if (!agentJar.exists())
{
throw new IllegalArgumentException(agentJar + " does not exist!");
}

final String processId = args[1];
if (Strings.isEmpty(processId))
{
throw new IllegalArgumentException("no PID provided!");
}

final String command = args[2];
switch (command)
{
case START_COMMAND:
{
for (int i = 3; i < args.length; i++)
{
SystemUtil.loadPropertiesFile(PropertyAction.PRESERVE, args[i]);
}

final EnumMap<ConfigOption, String> configOptions = fromSystemProperties();
final String agentArgs = buildAgentArgs(configOptions);
attachAgent(START_COMMAND, agentJar, processId, agentArgs);
out.println("Logging started.");
break;
}

case STOP_COMMAND:
{
attachAgent(STOP_COMMAND, agentJar, processId, command);
out.println("Logging stopped.");
break;
}

default:
throw new IllegalArgumentException("invalid command: " + command);
}
}

private static void printHelp()
{
out.println("Usage: <agent-jar> <java-process-id> <command> [property files...]");
out.println(" <agent-jar> - fully qualified path to the agent jar");
out.println(" <java-process-id> - PID of the Java process to attach an agent to");
out.println(" <command> - either '" + START_COMMAND + "' or '" + STOP_COMMAND + "'");
out.println(" [property files...] - an optional list of property files to configure logging options");
out.println("Note: logging options can be specified either via system properties or the property files.");
}

private static void attachAgent(
final String command, final File agentJar, final String processId, final String agentArgs)
{
try
{
ByteBuddyAgent.attach(agentJar, processId, agentArgs);
}
catch (final Throwable t)
{
out.println("Command '" + command + "' failed, cause: " + getCause(t));
System.exit(-1);
}
}

private static Throwable getCause(final Throwable t)
{
Throwable cause = t;
while (null != cause.getCause())
{
cause = cause.getCause();
}
return cause;
}

}
Loading

0 comments on commit a2f7df5

Please sign in to comment.