
Intro #
Apache Flink is a stream processing framework designed to process continuous data in real time. It is commonly used for event processing, log analytics, fraud detection, monitoring, and data enrichment pipelines.
In this tutorial, we will build a simple geolocation enrichment pipeline using Apache Flink and the IP2Location Java library. The Flink job reads incoming CSV event files from a local directory, extracts the IP address from each event, looks up the geolocation data from a local IP2Location BIN database, and writes the enriched records to an output directory. This setup runs on a single Debian 13 server, making it easy to test without Kafka, Hadoop, or a multi-node cluster.
For this tutorial, use this architecture:

This is realistic for a single-machine demo because Flink’s file source can monitor a directory for new files. The official Flink text-file source supports continuous reading by monitoring a directory for newly added files.
For the IP2Location side, use a local BIN database instead of calling an API for every row. The IP2Location Java library requires an IP2Location BIN database, and the lookup is performed locally using IP2Location.IPQuery().
Prerequisites #
- Flink mode: single-machine local cluster
- Java: Debian default JDK
- Build tool: Maven
- Input: CSV files in local folder
- Output: enriched CSV files in local folder
- IP2Location lookup: local BIN file
Apache Flink’s current local installation guide says local installation requires Java 11, 17, or 21. On Debian 13, the default-jdk package depends on openjdk-21-jdk, so this guide uses the Debian 13 default JDK for a simple tutorial setup.
Install required packages #
Run the below to install needed packages:
sudo apt update
sudo apt install -y default-jdk maven unzip
Verify Java and Maven:
java -version
javac -version
mvn -version

Expected Java version should be OpenJDK 21 on Debian 13.
Download and install Apache Flink #
Apache Flink 2.2.1 is currently listed on the official downloads page, and the Apache mirror page provides the binary archive path for flink-2.2.1-bin-scala_2.12.tgz.
Run the below to download and install:
cd /opt
sudo wget https://dlcdn.apache.org/flink/flink-2.2.1/flink-2.2.1-bin-scala_2.12.tgz
sudo tar -xzf flink-2.2.1-bin-scala_2.12.tgz
sudo ln -s flink-2.2.1 flink
sudo chown -R $USER:$USER /opt/flink-2.2.1 /opt/flink
Add Flink to your shell path:
echo 'export FLINK_HOME=/opt/flink' >> ~/.bashrc
echo 'export PATH=$FLINK_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
Start the local Flink cluster:
start-cluster.sh

Check the Flink dashboard:
http://<SERVER_IP>:8081
Or from the same machine:
http://localhost:8081
The Flink local installation guide also uses start-cluster.sh and the Web UI on port 8081 to verify the cluster.

Prepare the IP2Location BIN database #
Create a folder for the BIN database:
sudo mkdir -p /opt/ip2location
sudo chown -R $USER:$USER /opt/ip2location
Download an IP2Location LITE BIN database, or use a commercial IP2Location BIN database.
In our case, we’ll be using the DB26 IPv6 BIN file, so we’ll place the BIN file here:
/opt/ip2location/IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE-ADDRESSTYPE-CATEGORY-DISTRICT-ASN.BIN
Use an IPv4 BIN file if you only need to handle IPv4 addresses. Otherwise, use an IPv6 BIN file if you want to support both IPv4 and IPv6 lookups.
Create input and output folders #
Create folders to store our input CSV files and the resulting output CSV files.
mkdir -p ~/flink-ip2location-demo/input
mkdir -p ~/flink-ip2location-demo/output
Important: for this tutorial, do not append new rows into the same CSV file. Instead, add a new CSV file whenever you want Flink to process new data.
Good:
input/events_001.csv
input/events_002.csv
input/events_003.csv
Avoid:
input/events.csv
where rows are continuously appended into the same file.
Create the Maven project #
Let’s create the Maven project and add in our dependencies.
mkdir -p ~/flink-ip2location-demo/src/main/java/com/example
cd ~/flink-ip2location-demo
Create pom.xml:
cat > pom.xml <<'EOF'
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>ip2location-flink-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<flink.version>2.2.1</flink.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Flink APIs are provided by the Flink cluster at runtime -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<!-- FileSource and FileSink connector -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-files</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- IP2Location Java component -->
<dependency>
<groupId>com.ip2location</groupId>
<artifactId>ip2location-java</artifactId>
<version>8.13.0</version>
</dependency>
</dependencies>
<build>
<finalName>ip2location-flink-demo</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.IP2LocationEnrichmentJob</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
EOF
The IP2Location Java dependency is currently available from Maven Central as com.ip2location:ip2location-java:8.13.0. The Flink text-file format requires the flink-connector-files dependency.
Create the Flink Java job #
Create the Java file:
cat > src/main/java/com/example/IP2LocationEnrichmentJob.java <<'EOF'
package com.example;
import com.ip2location.IP2Location;
import com.ip2location.IPResult;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.common.serialization.SimpleStringEncoder;
import org.apache.flink.api.common.functions.OpenContext;
import org.apache.flink.connector.file.sink.FileSink;
import org.apache.flink.connector.file.src.FileSource;
import org.apache.flink.connector.file.src.reader.TextLineInputFormat;
import org.apache.flink.core.fs.Path;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import java.time.Duration;
public class IP2LocationEnrichmentJob {
public static void main(String[] args) throws Exception {
if (args.length < 3) {
System.err.println("Usage: IP2LocationEnrichmentJob <inputDir> <outputDir> <binFile>");
System.err.println("Example:");
System.err.println(" flink run target/ip2location-flink-demo.jar " +
"/home/user/flink-ip2location-demo/input " +
"/home/user/flink-ip2location-demo/output " +
"/opt/ip2location/IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE-ADDRESSTYPE-CATEGORY-DISTRICT-ASN.BIN");
return;
}
String inputDir = args[0];
String outputDir = args[1];
String binFile = args[2];
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// Keep the tutorial simple and output easier to read.
env.setParallelism(1);
// Required for FileSink to finalize output files in streaming mode.
env.enableCheckpointing(5000);
FileSource<String> source = FileSource
.forRecordStreamFormat(new TextLineInputFormat(), new Path(inputDir))
.monitorContinuously(Duration.ofSeconds(2))
.build();
DataStream<String> lines = env.fromSource(
source,
WatermarkStrategy.noWatermarks(),
"csv-file-source"
);
DataStream<String> enriched = lines
.filter(line -> line != null && !line.trim().isEmpty())
.filter(line -> !line.startsWith("event_id,"))
.map(new EnrichWithIP2Location(binFile));
// Print to TaskManager log for easy demo.
enriched.print();
FileSink<String> sink = FileSink
.forRowFormat(new Path(outputDir), new SimpleStringEncoder<String>("UTF-8"))
.build();
enriched.sinkTo(sink);
env.execute("IP2Location Geolocation Enrichment Demo");
}
public static class EnrichWithIP2Location extends RichMapFunction<String, String> {
private final String binFile;
private transient IP2Location ip2Location;
public EnrichWithIP2Location(String binFile) {
this.binFile = binFile;
}
@Override
public void open(OpenContext openContext) throws Exception {
ip2Location = new IP2Location();
ip2Location.Open(binFile, true);
}
@Override
public String map(String line) throws Exception {
String[] fields = line.split(",", -1);
if (fields.length < 5) {
return escape(line) + ",INVALID_RECORD,,,,,,,";
}
String eventId = fields[0].trim();
String eventTime = fields[1].trim();
String ipAddress = fields[2].trim();
String pageUrl = fields[3].trim();
String userAgent = fields[4].trim();
IPResult result = ip2Location.IPQuery(ipAddress);
String status = result.getStatus();
if (!"OK".equals(status)) {
return String.join(",",
escape(eventId),
escape(eventTime),
escape(ipAddress),
escape(pageUrl),
escape(userAgent),
escape(status),
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
);
}
return String.join(",",
escape(eventId),
escape(eventTime),
escape(ipAddress),
escape(pageUrl),
escape(userAgent),
escape(status),
escape(result.getCountryShort()),
escape(result.getCountryLong()),
escape(result.getRegion()),
escape(result.getCity()),
escape(result.getISP()),
String.valueOf(result.getLatitude()),
String.valueOf(result.getLongitude()),
escape(result.getDomain()),
escape(result.getZipCode()),
escape(result.getTimeZone()),
escape(result.getNetSpeed()),
escape(result.getIDDCode()),
escape(result.getAreaCode()),
escape(result.getWeatherStationCode()),
escape(result.getWeatherStationName()),
escape(result.getMCC()),
escape(result.getMNC()),
escape(result.getMobileBrand()),
String.valueOf(result.getElevation()),
escape(result.getUsageType()),
escape(result.getAddressType()),
escape(result.getCategory()),
escape(result.getDistrict()),
escape(result.getASN()),
escape(result.getAS()),
escape(result.getASDomain()),
escape(result.getASUsageType()),
escape(result.getASCIDR())
);
}
@Override
public void close() {
if (ip2Location != null) {
ip2Location.Close();
}
}
private static String escape(String value) {
if (value == null) {
return "";
}
String escaped = value.replace("\"", "\"\"");
if (escaped.contains(",") || escaped.contains("\"") || escaped.contains("\n")) {
return "\"" + escaped + "\"";
}
return escaped;
}
}
}
EOF
This job uses TextLineInputFormat to read CSV lines from new files. Flink’s own example shows FileSource.forRecordStreamFormat(new TextLineInputFormat(), path).monitorContinuously(...) for continuous directory monitoring.
Build the job JAR #
cd ~/flink-ip2location-demo
mvn clean package
Check the JAR:
ls -lh target/ip2location-flink-demo.jar

Start the Flink job #
Submit the job:
flink run \
target/ip2location-flink-demo.jar \
~/flink-ip2location-demo/input \
~/flink-ip2location-demo/output \
/opt/ip2location/IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE-ADDRESSTYPE-CATEGORY-DISTRICT-ASN.BIN
The job will keep running and watch the input directory.

Demonstrate the input #
Open another SSH session or terminal.
Create the first input file:
cat > ~/flink-ip2location-demo/input/events_001.csv <<'EOF'
event_id,event_time,ip_address,page_url,user_agent
1001,2026-06-22 10:00:01,8.8.8.8,/checkout,Chrome
1002,2026-06-22 10:00:05,1.1.1.1,/login,Firefox
1003,2026-06-22 10:00:09,208.67.222.222,/pricing,Edge
EOF
Wait a few seconds. Flink should detect the new file, process the rows, and enrich each IP address.
Then create a second file to demonstrate continuous processing:
cat > ~/flink-ip2location-demo/input/events_002.csv <<'EOF'
event_id,event_time,ip_address,page_url,user_agent
1004,2026-06-22 10:01:01,9.9.9.9,/docs,Chrome
1005,2026-06-22 10:01:06,4.2.2.2,/signup,Safari
EOF
View the output #
List the output folder:
find ~/flink-ip2location-demo/output -type f
The file names may look like this:

Some files may temporarily appear as .inprogress while the streaming sink is writing. In streaming mode, Flink’s FileSink finalizes files after successful checkpoints, so checkpointing should remain enabled.
Read the output:
find ~/flink-ip2location-demo/output -type f ! -name "*.inprogress*" -exec cat {} \;
You should see something like:
1004,2026-06-22 10:01:01,9.9.9.9,/docs,Chrome,OK,US,United States of America,California,Berkeley,QUAD9,37.87159,-122.27275,quad9.net,94709,-07:00,T1,1,510,USCA0010,Albany,-,-,-,52.0,DCH,A,IAB19-11,Alameda County,19281,QUAD9,quad9.net,DCH,9.9.9.0/24
1005,2026-06-22 10:01:06,4.2.2.2,/signup,Safari,OK,US,United States of America,Georgia,Atlanta,Level 3 Communications Inc.,33.748795,-84.38754,level3.com,30383,-04:00,T1,1,404,USGA0028,Atlanta,-,-,-,307.0,DCH,A,IAB19-11,Fulton County,3356,Level 3 Parent LLC,level3.com,DCH,4.0.0.0/14
1001,2026-06-22 10:00:01,8.8.8.8,/checkout,Chrome,OK,US,United States of America,California,Mountain View,Google LLC,37.38605,-122.08385,google.com,94035,-07:00,T1,1,650,USCA0746,Mountain View,-,-,-,32.0,DCH,A,IAB19-11,Santa Clara County,15169,Google LLC,google.com,DCH,8.8.8.0/24
1002,2026-06-22 10:00:05,1.1.1.1,/login,Firefox,OK,AU,Australia,Queensland,Brisbane,APNIC and CloudFlare DNS Resolver Project,-27.46754,153.02809,cloudflare.com,4000,+10:00,T1,61,07,ASXX0016,Brisbane,-,-,-,16.0,CDN,A,IAB19-11,Brisbane,13335,CloudFlare Inc,cloudflare.com,CDN,1.1.1.0/24
1003,2026-06-22 10:00:09,208.67.222.222,/pricing,Edge,OK,US,United States of America,California,San Jose,Cisco OpenDNS LLC,37.33939,-121.89496,opendns.com,95134,-07:00,T1,1,408,USCA0993,San Jose,-,-,-,24.0,CDN,A,IAB19-11,Santa Clara County,36692,Cisco OpenDNS LLC,opendns.com,CDN,208.67.222.0/24

The exact geolocation values depend on the IP2Location BIN edition and database release date.
Stop the job and cluster #
List running jobs:
flink list
Cancel the job:
flink cancel <JOB_ID>
Stop the local Flink cluster:
stop-cluster.sh
Conclusion #
The above is just a simple demonstration of how you can perform geolocation data enrichment in Apache Flink using IP2Location BIN database. For production, just modify the codes for your own streaming sources.
