Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Java Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

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 in.

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 into 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 “<Your API Token>” with the token that was obtained earlier, and then we're ready to run. As soon as we do, if everything is correct, our plugin will check the local drives and message the Slack if there are any errors.

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 over on GitHub.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Pascal
Pascal
6 months ago

Nicely written and well explained.

One question I have though is where and how is the bot deployed? Or it just works for another user even though the code is on my local computer?

Loredana Crusoveanu
5 months ago
Reply to  Pascal

Hey Pascal,
If you’re only sending data to Slack, you can deploy it anywhere. However, if you need some interaction (for example, when the user clicks a button, custom command, etc.), Slack should be able to access your application.
Cheers.

Shabbir
Shabbir
5 months ago

Nice Article.

You have created a New Slack App or Classic Slack App?

I created a new Slack App and using your article to create a Plugin. I found that with the new Slack App while Posting the message as a Bot from the “as_user” attribute is deprecated. and it is throwing below error.

{
“ok” : false,
“error” : “invalid_arguments”,
“deprecated_argument” : “as_user”,
“warning” : “missing_charset”,
“response_metadata” : {
“warnings” : [ “missing_charset” ]
}
}
Exception in thread “main” java.lang.IllegalStateException: SlackError{type=UNKNOWN, error=invalid_arguments}

Could you please help how to post Message as a Bot from this service.

Loredana Crusoveanu
3 months ago
Reply to  Shabbir

Hey Shabbir,
The Slack SDK referred to in the tutorial uses the Legacy API.
However, as this GitHub issue – https://github.com/HubSpot/slack-client/issues/147 – points out, support for as_user parameter for new apps would be available in their 1.7 release.
Cheers.

Comments are closed on this article!