1. Introduction
Slack is a popular chat system used by people and companies around the world. One of the things that makes it so popular is the ability to write our own custom plugins that can interact with people and channels within a single slack. This uses their HTTP API.
Slack doesn’t offer an official SDK for writing plugins with Java. However, there is an officially endorsed community SDK that we are going to use. This gives us access to almost all of the Slack API from a Java codebase without our needing to concern ourselves with the exact details of the API.
We’ll make use of this to build a small system monitoring bot. This will periodically retrieve the disk space for the local computer and alert people if any drives are getting too full.
2. Obtaining API Credentials
Before we can do anything with Slack, we need to create a new App and a Bot and connect it to our channels.
Firstly, let’s visit https://api.slack.com/apps. This is the base from where we manage our Slack apps. From here we can create a new app.
When we do this, we need to enter a name for the app and a Slack workspace to create it.
Once we’ve done this, the app has been created and is ready for us to work with. The next screen allows us to create a Bot. This is a fake user that the plugin will be acting as.
As with any normal user, we need to give this a display name and a username. These are the settings that other users in the Slack workspace will see for this bot user if they ever interact with it.
Now that we’ve done this, we can select “Install App” from the side menu and add the App to our Slack workspace. Once we’ve done this, the app can interact with our workspace.
This will then give us the tokens that we need for our plugin to communicate with Slack.
Each bot interacting with a different Slack workspace will have a different set of tokens. Our application needs the “Bot User OAuth Access Token” value for when we run it.
Finally, we need to invite the bot to any channels it should be involved in. This works by simply messaging it from the channel — @system_monitoring in this case.
3. Adding Slack to Our Project
Before we can use it, we first need to add the Slack SDK dependencies to our pom.xml file:
<dependency>
<groupId>com.hubspot.slack</groupId>
<artifactId>slack-base</artifactId>
<version>${slack.version}</version>
</dependency>
<dependency>
<groupId>com.hubspot.slack</groupId>
<artifactId>slack-java-client</artifactId>
<version>${slack.version}</version>
</dependency>
3. Application Structure
The core of our application is the ability to check for errors in the system. We’ll represent this with the concept of an Error Checker. This is a simple interface with a single method, triggered to check for errors and report them:
public interface ErrorChecker {
void check();
}
We also want to have the means to report any errors that have been found. This is another simple interface that will take a problem statement and report it appropriately:
public interface ErrorReporter {
void reportProblem(String problem);
}
The use of an interface here allows us to have different ways of reporting problems. For example, we might have one that sends emails, contacts an error reporting system, or sends messages to our Slack system for people to get an immediate notification.
The design behind this is that each ErrorChecker instance is given its own ErrorReporter to use. This gives us the flexibility to have different error reporters for different checkers to use because some errors might be more important than others. For example, if the disks are over 90% full that may require a message to a Slack channel, but if they are over 98% full then we might instead want to send private messages to specific people instead.
4. Checking Disk Space
Our error checker will check the amount of disk space on the local system. Any file system that has less than a particular percentage free is considered to be an error and will be reported as such.
We’ll make use of the NIO2 FileStore API introduced in Java 7 to obtain this information in a cross-platform manner.
Now, let’s take a look at our error checker:
public class DiskSpaceErrorChecker implements ErrorChecker {
private static final Logger LOG = LoggerFactory.getLogger(DiskSpaceErrorChecker.class);
private ErrorReporter errorReporter;
private double limit;
public DiskSpaceErrorChecker(ErrorReporter errorReporter, double limit) {
this.errorReporter = errorReporter;
this.limit = limit;
}
@Override
public void check() {
FileSystems.getDefault().getFileStores().forEach(fileStore -> {
try {
long totalSpace = fileStore.getTotalSpace();
long usableSpace = fileStore.getUsableSpace();
double usablePercentage = ((double) usableSpace) / totalSpace;
if (totalSpace > 0 && usablePercentage < limit) {
String error = String.format("File store %s only has %d%% usable disk space",
fileStore.name(), (int)(usablePercentage * 100));
errorReporter.reportProblem(error);
}
} catch (IOException e) {
LOG.error("Error getting disk space for file store {}", fileStore, e);
}
});
}
}
Here, we’re obtaining the list of all file stores on the local system and then checking each one individually. Any that has less than our defined limit as usable space will generate an error using our error reporter.
5. Sending Errors to Slack Channels
We now need to be able to report our errors. Our first reporter will be one that sends messages to a Slack channel. This allows anyone in the channel to see the message, in the hope that somebody will react to it.
This uses a SlackClient, from the Slack SDK, and the name of the channel to send the messages to. It also implements our ErrorReporter interface so that we can easily plug it into whichever error checker wants to use it:
public class SlackChannelErrorReporter implements ErrorReporter {
private SlackClient slackClient;
private String channel;
public SlackChannelErrorReporter(SlackClient slackClient, String channel) {
this.slackClient = slackClient;
this.channel = channel;
}
@Override
public void reportProblem(String problem) {
slackClient.postMessage(
ChatPostMessageParams.builder()
.setText(problem)
.setChannelId(channel)
.build()
).join().unwrapOrElseThrow();
}
}
6. Application Wiring
We are now in a position to wire up the application and have it monitor our system. For the sake of this tutorial, we’re going to use the Java Timer and TimerTask that are part of the core JVM, but we could just as easily use Spring or any other framework to build this.
For now, this will have a single DiskSpaceErrorChecker that reports any disks that are under 10% usable to our “general” channel, and which runs every 5 minutes:
public class MainClass {
public static final long MINUTES = 1000 * 60;
public static void main(String[] args) throws IOException {
SlackClientRuntimeConfig runtimeConfig = SlackClientRuntimeConfig.builder()
.setTokenSupplier(() -> "<Your API Token>")
.build();
SlackClient slackClient = SlackClientFactory.defaultFactory().build(runtimeConfig);
ErrorReporter slackChannelErrorReporter = new SlackChannelErrorReporter(slackClient, "general");
ErrorChecker diskSpaceErrorChecker10pct =
new DiskSpaceErrorChecker(slackChannelErrorReporter, 0.1);
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
diskSpaceErrorChecker10pct.check();
}
}, 0, 5 * MINUTES);
}
}
We need to replace “
7. Sending Errors as Private Messages
Next, we’re going to add an error reporter that sends private messages instead. This can be useful for more urgent errors since it will immediately ping a specific user instead of relying on someone in the channel to react.
Our error reporter here is more complicated because it needs to interact with a single, targeted user:
public class SlackUserErrorReporter implements ErrorReporter {
private SlackClient slackClient;
private String user;
public SlackUserErrorReporter(SlackClient slackClient, String user) {
this.slackClient = slackClient;
this.user = user;
}
@Override
public void reportProblem(String problem) {
UsersInfoResponse usersInfoResponse = slackClient
.lookupUserByEmail(UserEmailParams.builder()
.setEmail(user)
.build()
).join().unwrapOrElseThrow();
ImOpenResponse imOpenResponse = slackClient.openIm(ImOpenParams.builder()
.setUserId(usersInfoResponse.getUser().getId())
.build()
).join().unwrapOrElseThrow();
imOpenResponse.getChannel().ifPresent(channel -> {
slackClient.postMessage(
ChatPostMessageParams.builder()
.setText(problem)
.setChannelId(channel.getId())
.build()
).join().unwrapOrElseThrow();
});
}
}
What we have to do here is to find the user that we are messaging — looked up by email address, since this is the one thing that can’t be changed. Next, we open an IM channel to the user, and then we post our error message to that channel.
This can then be wired up in the main method, and we will alert a single user directly:
ErrorReporter slackUserErrorReporter = new SlackUserErrorReporter(slackClient, "[email protected]");
ErrorChecker diskSpaceErrorChecker2pct = new DiskSpaceErrorChecker(slackUserErrorReporter, 0.02);
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
diskSpaceErrorChecker2pct.check();
}
}, 0, 5 * MINUTES);
Once done, we can run this up and get private messages for errors as well.
8. Conclusion
We’ve seen here how we can incorporate Slack into our tooling so that we can have feedback sent to either the entire team or to individual members. There’s much more we can do with the Slack API, so why not see what else we can incorporate?
As usual, the source code for this article can be found on GitHub.