Parsing Apache Logs with tail, cut, sort, and uniq

A client experienced some intermittent website down time last week during the final few days of April 2021, and sent over that month’s Apache logs for me to see if there is anything out of the ordinary – excessive crawling, excessive probing, brute force password attacks, things of that nature. Below are a few commands I have used that I thought would be nice to keep handy for future uses. I am currently using Ubuntu 20.04 LTS.

While unrelated, just to form a complete picture, my client sent the logs to me in gz-compressed format. If you are not familiar on how to uncompress it, it is fairly straight forward:

gunzip 2021-APR.gz

Back on topic… I ended on parsing the file in three separate ways for me to get an overall view of things. I found that the final few days of April are represented in the roughly final 15,000 lines of the log file, so I decided to use the tail command as my main tool.

First, I did following command below to find which IP addresses hit the server the most:

tail -n 15000 filename.log | cut -f 1 -d ' ' | sort | uniq -c | sort -nr | more

Quick explanation:

  • The tail command pulls the final 15,000 lines from the log file (final few days of the month)
  • The cut command parses each line using a space delimiter and returns the first field (the IP addres)
  • The sort command sorts the results thus far
  • The uniq command groups the results thus far and provides a count
  • The second sort command reverse the sort so the highest result is on top
  • Finally, the more command creates screen-sized pagination so it’s easier to read

There is always more than one way to do something in Linux, of course. Just as an aside, the following functions very similarly:

cat filename.log | awk '{print $1}' | sort | uniq -c | sort -nr | tail -15000 | more

Then, I thought it would be nice to get an idea of how many requests were made per hour. This can be achieved with the command below.

tail -n 15000 filename.log | cut -f 4 -d ' ' | cut -f 1,2 -d ':' | sort | uniq -c | more

The main difference here is that I opted for the 4th (rather than 1st) result of the cut command, which gets me the timestamp element (rather than IP address), and then a second cut command parses it on the colon symbol and returns the first (date) and second (hour) for further grouping.

Finally, I tweaked it a little bit more so I get an idea of whether there was excessive requests within any minute-span. This can be achieved by expanding the second cut command slightly, as per below.

tail -n 15000 filename.log | cut -f 4 -d ' ' | cut -f 1,2,3 -d ':' | sort | uniq -c | more

Querying Dates and Date Parts in Laravel

The World War II timeline application allows users to query historical events by a specific date, by month/day combination, by a year, or they can leave everything blank and the system will show them events that had happened “on this day”. Here’s an example for 18 March 1944. To achieve this, in SQL speak, it would be something like the following.

-- By specific date
select * from my_table where my_date=:yyyymmdd;

-- By year
select * from my_table where date_format(my_date, '%Y')=:yyyy;

In Laravel’s This can be achieved with several Laravel Eloquent’s built in functions: whereDate(), whereYear(), whereMonth(), and whereDay(). Below are some examples from WW2DB’s TimelineController.

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Timeline;

class TimelineController extends Controller {

	/* ... Unrelated code omitted from this article */

	public function getList(Request $request) {
		/* ... Unrelated code omitted from this article */

		$mm = $request->query('mm');
		$dd = $request->query('dd');
		$yyyy = $request->query('yyyy');
		$timeline = Timeline::orderBy('my_date');
		// If querying by specific date, use whereDate()
		$timeline = $timeline->whereDate('my_date', date($yyyy."-".$mm."-".$dd));
		// If querying by month/day, use whereMonth() + whereDay()
		$timeline = $timeline->whereMonth('my_date', $mm)
			->whereDay('my_date', $dd);

		// If querying by year, use whereYear()
		$timeline = $timeline->whereYear('my_date', $yyyy);
		$timeline = $timeline->get();
		return view('timeline.my_view', compact('timeline'));

	/* ... Unrelated code omitted from this article */

Or, simplified whereMonth() + whereDay() example to get data for 18 March:

$mm = "03";
$dd = "18";
$timeline = Timeline::orderBy('my_date');		
$timeline = $timeline->whereMonth('my_date', $mm)
	->whereDay('my_date', $dd);
$timeline = $timeline->get();

Submitting Multiple Products and Variable Quantities to PayPal

I recently did some work for a small local non-profit group to accept payment over PayPal. They have multiple, but limited, offerings that they were selling to raise funds, and customers could purchase in variable quantities. They originally used the buttons auto-generated by PayPal, but that functionality forces their customers to go back and forth between the single-page sales page and the PayPal shopping cart, which creates too much complexity for their simple sales process. Below is a simplified version of what I put together for them.

First, this is the sales form:

	<form method="post">
		<table border="0">
			<tr><td>Name</td><td>&nbsp;</td><td><input type="text" name="name" id="name" maxlength="100" /></td></tr>
			<tr><td>Email</td><td>&nbsp;</td><td><input type="text" name="email" id="email" maxlength="100" /></td></tr>
			<tr><td>Phone</td><td>&nbsp;</td><td><input type="text" name="phone" id="phone" maxlength="100" /></td></tr>
			<tr><td colspan="9"><br />I wish to purchase:<br /><br /></td></tr>
			<tr><td>Product 1</td><td>&nbsp;</td><td><select name="prod1"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $5 ea.</td></tr>
			<tr><td>Product 2</td><td>&nbsp;</td><td><select name="prod2"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $5 ea.</td></tr>
			<tr><td>Product 3</td><td>&nbsp;</td><td><select name="prod3"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $10 ea.</td></tr>
			<tr><td>Product 4</td><td>&nbsp;</td><td><select name="prod4"><option value="0">0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option></select> @ $20 ea.</td></tr>
		<input type="submit" value="Review Order" />

The next page grabs the values passed in from the previous page and displays a confirmation screen for the customer to review; if all looks ok, the customer will submit the following form. Note the specific form action and hidden inputs (cmd, upload, business, and currency_code) required by PayPal. Multiple products is achieved by creating sets of inputs (item_name_*, quantity_*, amount_*) for each product, with the asterisk being a number; the numbering must start from 1 and subsequent products must be in order, ie. 2, 3, 4, etc., thus the use of the IF statements and the $itemCnt counter variable. The amount_* field represents the unit price of the associated product, not the line total.

	<form action="" method="post">
		<input type="hidden" name="cmd" value="_cart">
		<input type="hidden" name="upload" value="1">
		<input type="hidden" name="business" value="">
		<input type="hidden" name="currency_code" value="USD">
		$itemCnt = 1;
		if ($prod1 != "0") {
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 1">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod1); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="5">
		if ($prod2 != "0") {
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 2">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod2); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="5">
		if ($prod3 != "0") {
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 3">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod3); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="10">
		if ($prod4 != "0") {
			<input type="hidden" name="item_name_<?php print($itemCnt); ?>" value="Product 4">
			<input type="hidden" name="quantity_<?php print($itemCnt); ?>" value="<?php print($prod4); ?>">
			<input type="hidden" name="amount_<?php print($itemCnt); ?>" value="20">
		// Repeat as necessary...

Connecting PHP to Oracle with OCI8

Below is the set of steps I took to connect a XAMPP setup to an Oracle database server. I am doing this on a server running Windows Server 2016.

  1. Download and install XAMPP. I chose the one bundled with the 32-bit version of PHP 7.3.2 with thread safety. Sourceforge: xampp-win32-7.3.2-0-VC15-installer.exe
  2. Download and install Oracle Instant Client. Because the PHP architecture is 32-bit, Oracle Instant Client must also be 32-bit. Take note of your Oracle server’s version, and install the appropriate client. Oracle: Oracle Instant Client Downloads
  3. Set up your sqlnet.ora and tnsnames.ora files at %ORACLE_HOME%\network\admin\. These two files together will tell your Oracle Instant Client how to get to your Oracle server. Sample sqlnet.ora:

    Sample tnsnames.ora:

    			(ADDRESS = (PROTOCOL = TCP)(HOST = = 1521))
    			(SERVICE_NAME = db)
  4. In Windows, add the folder of Oracle Instant Client to the PATH system variable. Create a new system variable called ORACLE_HOME and put the same path in there as well.
  5. Download the OCI8 package. I chose the one labeled “7.3 Thread Safe (TS) x86” in order to match my PHP version, thread safe configuration, and the 32-bit architecture. Once downloaded, extract either php_oci8.dll, php_oci8_11g.dll, or php_oci8_12c.dll (depending on your Oracle server version) and place the DLL file in the PHP Extensions directory. By default, that folder for my installation of XAMPP is C:\xampp\php\ext\. PHP PECL: OCI8 for Windows
  6. Launch XAMPP Control Panel and start the Apache server. Add phpinfo(); to one of your pages to see if OCI8 has its own section; if so, you are ready to make your connection to your Oracle server. Here’s a quick and dirty PHP snippet you can use to do a quick connection test:
    $conn = oci_connect($user, $password, $server);
    $sql = "select user from dual";
    $stid = oci_parse($conn, $sql);
    $row = oci_fetch_array($stid, OCI_BOTH);

Recurse through a directory with PHP

The PHP code below goes through a directory/folder recursively and display the files found as HTML links on a web page.

// Configuration
$cDrivePath = "C:\\inetpub\\site3\\filelibrary\\"; // Be sure to include slash at the end!
$webPath = "/filelibrary/"; // Be sure to include slash at the end!
// End configuration

function cleanUpFileName($fName, $removeThisString) {
	$ret = $fName;
	$ret = str_replace($removeThisString, "", $ret);
	$ret = str_replace("\\", " > ", $ret);
	return $ret;

function printDirContents(
		$path, 			// This is the HDD path to recurse into
		$cDrivePath, 	// Always carry the $cDrivePath value; used for beautifying text
		$webPath		// Always carry the $webPath value; used for beautifying text
	) {

	// Make sure we have a trailing slash and asterix
	$path = rtrim($path, "\\") . "\\*";

	// Make an array to hold all the sub-directories
	$dirs = glob($path, GLOB_ONLYDIR);
	// Make an array to hold all the files
	$files = glob($path); //This contains both sub-directories and files
	$files = array_diff($files, $dirs); // Remove sub-directories from the array
	// Print files
	foreach ($files as &$f) {
		$fnDownload = str_replace($cDrivePath, "", $f);
		$fnDisplay = cleanUpFileName($f, str_replace("*","",$path));
		print("[a href='".$webPath.$fnDownload."']".$fnDisplay."[/a]");
	// Recurse into sub-directories
	foreach ($dirs as &$d) {
		print("[h3]".(cleanUpFileName($d, $cDrivePath))."[/h3]";
		// Print folder
		printDirContents($d, $cDrivePath, $webPath);
$displayArray = printDirContents($cDrivePath, $cDrivePath, $webPath);

PowerShell script for exporting Print Server info to Oracle

The example below covers how a PowerShell script can connect to a Windows Print Server, how it can query printers information, and how it can connect to an Oracle database to perform a query.

# Start configuring parameters
Param (
	[string]$Printservers = "printserver1",
	[string]$OracleServer = "orcl",
	[string]$OracleUser = "scott",
	[string]$OraclePassword = "tiger"
	[string]$sql = "insert into printer_list (print_server, printer_name, printer_location, printer_comment, printer_ip, printer_driver_name, printer_driver_version, printer_driver, entry_dt) values(:print_server, :printer_name, :printer_location, :printer_comment, :printer_ip, :printer_driver_name, :printer_driver_version, :printer_driver, sysdate) "
# End configuring parameters

ForEach ($Printserver in $Printservers) { # Start looping through each print server
	$Printers = Get-WmiObject Win32_Printer -ComputerName $Printserver

	ForEach ($Printer in $Printers) { # Start looping through each printer
		$connectionString = "User Id=$OracleUser;Password=$OraclePassword;Data Source=$OracleServer;"
		$connection = $null
		$command = $null

		Try {
			$connection = New-Object System.Data.OracleClient.OracleConnection($connectionString)
			$command = New-Object System.Data.OracleClient.OracleCommand -ArgumentList $sql, $connection
			$NoOutput = $command.Parameters.Add("print_server", $Printserver)
			$NoOutput = $command.Parameters.Add("printer_name", $Printer.Name)
			$Location = $Printer.Location
			if (!$Location) {
				$Location = " "
			$NoOutput = $command.Parameters.Add("printer_location", $Location)
			$Comment = $Printer.Comment
			if (!$Comment) {
				$Comment = " "
			$NoOutput = $command.Parameters.Add("printer_comment", $Comment)
			$NoOutput = $command.Parameters.Add("printer_ip", $Printer.Portname)
			$Drivers = Get-WmiObject Win32_PrinterDriver -Filter "__path like '%$($Printer.DriverName)%'" -ComputerName $Printserver
			$DriverVersion = " "
			$Driver = " "
			ForEach ($Driver in $Drivers) {
				$Drive = $Driver.DriverPath.Substring(0,1)
				$DriverVersion = (Get-ItemProperty ($Driver.DriverPath.Replace("$Drive`:","\\$PrintServer\$Drive`$"))).VersionInfo.ProductVersion
				$Driver = Split-Path $Driver.DriverPath -Leaf
			$Drivername = $Printer.Drivername
			if (!$Drivername) {
				$Drivername = " "
			$NoOutput = $command.Parameters.Add("printer_driver_name", $Drivername)
			$NoOutput = $command.Parameters.Add("printer_driver_version", $DriverVersion)
			$NoOutput = $command.Parameters.Add("printer_driver", $Driver)
		Finally {
			if ($connection -ne $null) {

			if ($command -ne $null) {
	} # End looping through each printer
} # End looping through each print server

Using VBScript to run Excel macro

I have a folder containing several Excel files, each containing a macro named “FETCH_LATEST_DATA”. The following steps will detail how I had scheduled a VBScript to run these Excel macros nightly.

First, I created the VBScript below that opens each Excel file successively and calls the particular macro. Note that I have disabled alerts and events since I intended to run it as an unattended job, and that I am using “Wscript.Echo” to output some start/end info for debugging purposes should something go wrong in the future.

excelFolder = "\\fileserver\share\folder"

Set objFso = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFso.GetFolder(excelFolder)

Set excelApp = CreateObject("Excel.Application")
excelApp.DisplayAlerts = False
excelApp.EnableEvents = False

for each objFile In objFso.GetFolder(excelFolder).files
	Wscript.Echo "Starting " & & " " & now
	filename =
	Set excelBook = excelApp.Workbooks.Open(excelFolder & "\" & filename)

	If excelBook.ReadOnly Then
		Wscript.Echo "Readonly mode detected, skipping"
		excelApp.Run "FETCH_LATEST_DATA"
	End if

	Wscript.Echo "Ending " & & " " & now
	excelBook.Close False
	set excelBook = Nothing

Set excelApp = Nothing
set objFso = nothing

Next, I inserted a line in my already-existing batch file that contains all the other jobs that I typically run on this Windows server every evening. Note that I am using “>>” to direct any console output (see “Wscript.Echo” above) to a log file should there be any debugging needs.

C:\windows\syswow64\cscript.exe C:\scripts\update_excel.vbs >> update_excel.log

Before we can run Excel unattended, we must create these two folders. If these two folders are missing, the scheduled job would sit perpetually in Windows Task Scheduler as if running, but it would never actually complete; in the way that the logging info is setup above, you would see something like “Starting file1.xlsm 6/20/2019 10:15:00 AM” without any additional lines being logged. If your Windows server is running in 32-bit mode, you can ignore the second folder.



Facebook App Review for Server-to-Server Apps

In a database table, I kept interesting World War II facts that took place on every date throughout the course of a calendar year. I had scheduled a Python script to make daily queries against this table with the current date, and then posts the result on my WW2DB Facebook Page via Facebook GraphAPI. This was deployed some years ago and had been working without a hitch.

In 2018, in response to the many security/privacy related problems that you undoubtedly had read about, Facebook tightened things up quite a bit. Understandably, my process was affected. Facebook informed me that I had to undergo an app review, particularly to justify my need for these two API calls:

  • manage_pages
  • publish_pages

I initially received this request for app review around July or August 2018. I immediately got to work. Surprisingly, I was repeatedly rejected, most of the time the reason was that I failed to show Facebook folks how a user logins in to Facebook using my app, despite I clearly noted that my application was a server-to-server app that had no user interface. I reached out to a good friend who works at Facebook just to see if he happens to know anyone who might know someone with this knowledge; he came back telling me that everyone he spoke to was equally stumped. Life got busy, and I forgot about this issue for a while. I did not pick it up until last week. After two more rejections, today, 5 February 2019, I finally got everything straightened out after more than half a year (but again, my own delay in late 2018 admittedly also dragged it on a bit).

You would imagine this to be a straight-forward process, right? I guess not! Hopefully, my notes below will help you shorten the time you need to get your server-to-server app approved.

First, the basics. Go to, find and click on your application. Then, click “Settings” on the left, and then “Basic”; in here, I have these fields populated: Display Name, Contact Email, Private Policy URL, Terms of Service URL, App Icon, and Category. On the bottom of the page, I added “Website” as my platform (note there is no option for a server-to-server application).

Then, go to “App Review” and then “Permissions and Features”. Scroll down to “manage_pages” and click on the “Request” button. Do the same for “publish_pages”. Then click the “Continue” link to fill out some info for each of the two.

For “publish_pages”:

  • “Tell us how you’re using this permission or feature” — I told them that this is a server-to-server application, explained how my Python script queries my database, and then uses Facebook GraphAPI to post to my own page.
  • “Demonstrate how your selected platforms will use this permission or feature” — I checked “web” (note there is no option for server-to-server), and then in the text box listed the 3 steps below, and then noted that I would be uploading a blank screencast (it was literally a 5-minute-long video of a black screen) because this server-to-server app has no user interface. I also offered to send them the source code of my script if Facebook wanted to look at it (in fact, in one of my failed attempts to secure permission, my screencast was actually showing my source code so they could have a developer confirm the code does what I described).
    1. My Python script queries my database for relevant entries for the current date
    2. The script then creates an instance of Facebook GraphAPI
    3. Use the “.put_object” method to make a post on my own page
  • Then, I uploaded the blank video aforementioned.

For “manage_pages”:

  • “Tell us how you’re using this permission or feature” — Copy/pasted the same as above.
  • “Demonstrate how your selected platforms will use this permission or feature” — I checked “Server-to-Server” option, and then copy/pasted the same text as above; less the mention of the blank video.

After saving those two, I submitted my request.

In my case, a business day or so later, I received word of my approval, but final implementation was pending me proving my business is legitimate. If you have this requirement as well, in addition to showing your business as legitimate, you will also want to show that your business phone number is legitimate. To satisfy these two requirements, I uploaded scans of:

  1. The Business Registration Certificate showing that my business had been properly registered with the government. In my case, it is the state of New Jersey in the United States; your document may be titled differently. This document shows my business name and mailing address, but it does not show my phone number.
  2. The contract I signed on behalf of my business renting a mailbox from the local UPS Store. This document shows my business name, mailing address, and phone number.
  3. United States Postal Service Application for Delivery of Mail Through Agent, which was signed by both the manager of the UPS Store and myself on behalf of my business. This document shows my business name, mailing address, and phone number. The UPS Store contract alone probably was enough, but I thought I would upload this, just in case.

While I understand there are a lot of shady elements on the web, and Facebook had been a victim of those elements, this whole process was not easy. Official instructions I found on Facebook site was slightly out-dated, and there was no way for me to get any human assistance. The end result was that Facebook’s reviewers had to spend extra time re-reviewing my repeated attempts, and I needed to spend extra time rewording my requests while trying to guess what Facebook wanted to see. In comparison, when Google (via its Adsense program) recently requested me to comply to GPDR, I was able to easily reach a human (via email), who clarified exactly what actions I needed to take. After I implemented the requirements, he even emailed me back to confirm my compliance. The experience with Google was much smoother when compared to Facebook’s.

Large listener.log file size causing Oracle Listener to fail

Recently, I ran into a case in which the user was attempting to log on to a database, but the log on process was simply churning and churning without coming to an end. When I tried to perform a TNSPING against it, that process also failed to return any results, or at least not in a reasonable amount of time.

Thinking that it might be caused by a dead listener, I went on to the server and attempted to use the LSNRCTL command to restart the listener. Surprisingly, I was encountered with a number of errors, including “TNS-12541: TNS:no listener” and “TNS-12560: TNS:protocol adapter error”.

I was able to find a much more knowledgeable DBA to locate root problem — On this Windows-hosted Oracle database server, the listener.log file had grown too large. Our solution:

1. Archive the old listener.log file
2. Create an empty file by the same name

After those two action, the listener was able to restart successfully, allowing user connections once again.

Implementing OpenStreetMap

Google recently announced a change in their Maps API usage quota that will inevitably move some current users from the previously free tier into a paid tier. I thought this might perhaps drive more developers to switch from Google Maps to OpenStreetMap if only for budget reasons, so hopefully the following guide would help get things started.

OpenStreetMap implementation is actually just as simple as Google Maps, especially when coupled with Javascript libraries focused on building maps. In the following example, I used Leaflet. As far as tiles are concerned, I used tiles by Stamen. While I personally like Stamen’s tiles a lot, there are many tiles available across the web with different styles, and some might fit your particular niche or style better; check out this link for a few others. Finally, for the data, I used some data from Lava’s WW2DB project.

To get things started, I created a div that will eventually contain the map. Then, I linked to the .css stylesheet and the .js package files:

<div id="mapdiv" style="width: 400px; height: 400px;"></div>
<link rel="stylesheet" href="" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin=""/>
<script src="" integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw==" crossorigin=""></script>

Then, a snippet of PHP code fetched necessary data from the WW2DB backend database, and used the data to build a Javascript array. The resulting Javascript array that I built looked something like this:

var rawDataArray = [
	{ "name": "This is marker #1", "lat": "10", "lng": "10" },
	{ "name": "This is marker #2", "lat": "20", "lng": "20" },
	{ "name": "This is marker #3", "lat": "30", "lng": "30" },

Now comes the main part. You will see that it is not too complex, even if have little experience with Google Maps, OpenStreetMap, or Leaflet.

// Initialize the map (parameters: initial latitude, initial longitude, zoom level)
var mymap ='mapdiv').setView([0, 0], 13);

// Specify tiles
L.tileLayer('https://stamen-tiles-{s}{z}/{x}/{y}.{ext}', {
	attribution: 'Tiles <a href="">Stamen Design</a>, <a href="">CC BY 3.0</a> — Data <a href="">OpenStreetMap</a>',
	subdomains: 'abcd',
	minZoom: 0,
	maxZoom: 18,
	ext: 'png'

// Loops through the raw data, place a point on the map, and build an array of the points
var circleArray = [];
for (var i = 0; i < rawDataArray.length; i++) {
	var rawData = rawDataArray[i];
	circle = new[rawData["lat"], rawData["lng"]], {
		color: 'red',
		fillColor: '#f03',
		fillOpacity: 0.5,
		radius: 50

// Adjust the map view based on the array of points, so the map won't appear to be zoomed overly in or out
var group = new L.featureGroup(circleArray);

Here's an example I implemented for showing WW2-era facilities found on the island of Taiwan:

While this worked for 99% of maps found on WW2DB, I discovered a small problem with Leaflet in which the .fitBounds() method did not handle things well when a single data set contains points on both sides of the International Date Line. To be fair, this is not truly a Leaflet problem; this issue is generally a headache across all mapping programs, although this is something I did not notice with Google Maps before. Here is an example demonstrating said problem involving points located in the Territory of Alaska:

I developed a work-around for this. Recall I used a PHP snippet to fetch data from the backend and to build the Javascript array for Leaflet to use. I modified the PHP code so that as I looped through the data, I would also keep counts of points with really low (I used 120 or less) or really high (I used 120 or more) longitude values. With this two counts, I could then have the PHP determine whether I would bring some points to the other side of the International Date Line by adding or subtracting 360 degrees to their longitude values. At this time, I made that determination by evaluating the number of points that would remain/would be moved. For the case of Alaska, with this logic applied, the PHP code would decide that all data points with longitude value less than 120 will have 360 added to it; for example, (54.359349,-159.66807) will become (54.359349,200.33193). After applying this work-around, the Alaska map becomes this: