Repository: FoundationDB/fdb-kubernetes-operator
Pull request #1686 (2023-06-19 14:06:33)

supporting multiple Log processes per disk
# Description

Fixes: https://github.com/FoundationDB/fdb-kubernetes-operator/issues/1370

## Type of change

*Please select one of the options below.*

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Documentation
- Other

## Discussion

*Are there any design details that you would like to discuss further?*

## Testing

*Please describe the tests that you ran to verify your changes. Unit tests?
Manual testing?*

*Do we need to perform additional testing once this is merged, or perform in a larger testing environment?*

## Documentation

*Did you update relevant documentation within this repository?*

*If this change is adding new functionality, do we need to describe it in our user manual?*

*If this change is adding or removing subreconcilers, have we updated the core technical design doc to reflect that?*

*If this change is adding new safety checks or new potential failure modes, have we documented and how to debug potential issues?*

## Follow-up

*Are there any follow-up issues that we should pursue in the future?*

*Does this introduce new defaults that we should re-evaluate in the future?*


foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 3887a73a49285b418580b8520a03313aab5d9bdb
* Duration 0:03:03
* Result: :x: **FAILED**
* Error: `Error while executing command: IMG=${REGISTRY}/${OPERATOR_IMAGE} make container-build container-push. Reason: exit status 2`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/434e6d01-32af-42f9-b464-25af6249758a.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr-kind on Linux CentOS 7

* Commit ID: c5248b877bbb4784582e01c5e6c99778014b9a3a
* Duration 0:06:33
* Result: :x: **FAILED**
* Error: `Error while executing command: make -C e2e kind-setup. Reason: exit status 2`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr-kind/90f741cf-e666-4747-94bf-fe7f84052450.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 52aefb48d22b77d0edbc849238b67fd2b39f29d7
* Duration 3:33:12
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/bb54ef36-7989-4b6a-ab95-4ad42cca3775.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/bb54ef36-7989-4b6a-ab95-4ad42cca3775/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr-kind on Linux CentOS 7

* Commit ID: 52aefb48d22b77d0edbc849238b67fd2b39f29d7
* Duration 4:07:39
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr-kind/28b5de08-b62e-4d96-84a5-3a554d8922a8.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr-kind/28b5de08-b62e-4d96-84a5-3a554d8922a8/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: c860196452bdb1f1fd1fb1d1ccbdcd64d7a59743
* Duration 4:07:08
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/b08ea008-b288-43c0-8b63-6f01c8980a8c.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/b08ea008-b288-43c0-8b63-6f01c8980a8c/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: a7ba5ae1d7c5eaba58cf7cd316bb4c4f8d2690aa
* Duration 4:06:40
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/acc5b214-7d0c-4f02-967b-1dfdd0f2ece5.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/acc5b214-7d0c-4f02-967b-1dfdd0f2ece5/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 51dc84b6732ad4161af58306362560105e310f8b
* Duration 4:06:33
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/e89480b5-7c44-4e1c-978a-72d4c9a698cf.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/e89480b5-7c44-4e1c-978a-72d4c9a698cf/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 7329fe51e2df029dcb81bb8c276b320719fd45ea
* Duration 2:31:32
* Result: :white_check_mark: **SUCCEEDED**
* Error: `N/A`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/cb441225-1dfc-4821-a7cb-b115f0fb0871.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/cb441225-1dfc-4821-a7cb-b115f0fb0871/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 79302702b313152a2af5b092e0e556e22af519db
* Duration 3:02:41
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/ec69e8bf-a2e7-452f-8c48-a4031b921e8f.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/ec69e8bf-a2e7-452f-8c48-a4031b921e8f/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 6b98165701e605055ffe833031c3ce2911e61f52
* Duration 2:28:00
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/41f771b8-4ccd-4ee8-ac04-84c20b39750f.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/41f771b8-4ccd-4ee8-ac04-84c20b39750f/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: d0b71e248041be9b5d27aba6039ee896240f0349
* Duration 3:37:34
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/23db46af-3a3b-42db-89e1-84b3609911fe.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/23db46af-3a3b-42db-89e1-84b3609911fe/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 9adb3a2632a96c8c5d50e4b5e2c40b3ef5ff6075
* Duration 2:23:10
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/eff89b75-b50e-44e7-86f5-802ed7b8a8cb.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/eff89b75-b50e-44e7-86f5-802ed7b8a8cb/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 722a020919e412687c1ac9d192e4d7f76fbc2ab6
* Duration 2:23:49
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/8a47cfb9-d1c6-4310-8a99-8c1dffd56dad.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/8a47cfb9-d1c6-4310-8a99-8c1dffd56dad/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 392e4d4d184a6349948db05aa7897b622ce0d95a
* Duration 2:26:04
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/8f031fc6-57f4-46d3-b444-1305d5b953f2.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/8f031fc6-57f4-46d3-b444-1305d5b953f2/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 699cdacf50bfbc02505ff42abd0abce7b7cd689b
* Duration 2:27:28
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/ecdff290-7bc1-4bd0-81b0-460c3b03338c.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/ecdff290-7bc1-4bd0-81b0-460c3b03338c/workspace.zip) zip file of the working directory (available for 30 days)

AnnigeriShambu: Test cases are passing. I will try again running PR builder now
```
Ran 25 of 34 Specs in 4451.201 seconds
SUCCESS! -- 25 Passed | 0 Failed | 8 Pending | 1 Skipped
--- PASS: TestOperatorCreationVelocity (4451.21s)
PASS
ok  	github.com/FoundationDB/fdb-kubernetes-operator/e2e/test_operator	4451.696s
```
AnnigeriShambu: test this please
foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 7c495dd1debd394999ad2a1679c15b4e4dac2a32
* Duration 4:06:46
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/4fb6cd7b-2c7a-4826-b585-6dd22b944c70.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/4fb6cd7b-2c7a-4826-b585-6dd22b944c70/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 7c495dd1debd394999ad2a1679c15b4e4dac2a32
* Duration 2:43:25
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/680a75bc-5861-418c-85fd-ba7f5b357876.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/680a75bc-5861-418c-85fd-ba7f5b357876/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 03de21d2f28c27bf25764dd89952a237555828c5
* Duration 2:39:53
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/62d76f82-f4fb-4b48-9f3b-8c75562ba51a.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/62d76f82-f4fb-4b48-9f3b-8c75562ba51a/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 5764a6dac51770d8e33f94a02b88f81a105265b3
* Duration 2:43:41
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/f1f58173-a98a-4fd7-a63b-e5725b91ec2c.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/f1f58173-a98a-4fd7-a63b-e5725b91ec2c/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 50951d183ba62f19711e22db220fc4086ac9e012
* Duration 2:28:00
* Result: :white_check_mark: **SUCCEEDED**
* Error: `N/A`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/8bc8213b-6763-4e6b-aa15-1fdfe21f2da8.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/8bc8213b-6763-4e6b-aa15-1fdfe21f2da8/workspace.zip) zip file of the working directory (available for 30 days)


Patch:
diff --git a/Makefile b/Makefile
index 649d75a2..863cddcb 100644
--- a/Makefile
+++ b/Makefile
@@ -159,7 +159,7 @@ fmt: bin/fmt_check
 # TODO johscheuer: enable those new command in a new PR.
 bin/fmt_check: ${GO_ALL}
 	# $(GO_LINES) -w .
-	go fmt .
+	go fmt $$(go list ./...)
 	# $(GO_IMPORTS) -w .
 	#$(GOLANGCI_LINT) run --fix
 	@mkdir -p bin
diff --git a/api/v1beta1/foundationdbcluster_types.go b/api/v1beta1/foundationdbcluster_types.go
index 38d4a3d3..2f618385 100644
--- a/api/v1beta1/foundationdbcluster_types.go
+++ b/api/v1beta1/foundationdbcluster_types.go
@@ -452,6 +452,10 @@ type FoundationDBClusterStatus struct {
 	// If there are more than one value in the slice the reconcile phase is not finished.
 	StorageServersPerDisk []int `json:"storageServersPerDisk,omitempty"`
 
+	// LogServersPerDisk defines the LogServersPerDisk observed in the cluster.
+	// If there are more than one value in the slice the reconcile phase is not finished.
+	LogServersPerDisk []int `json:"logServersPerDisk,omitempty"`
+
 	// ImageTypes defines the kinds of images that are in use in the cluster.
 	// If there is more than one value in the slice the reconcile phase is not
 	// finished.
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 8801d958..989453e8 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -771,6 +771,11 @@ func (in *FoundationDBClusterStatus) DeepCopyInto(out *FoundationDBClusterStatus
 		*out = make([]int, len(*in))
 		copy(*out, *in)
 	}
+	if in.LogServersPerDisk != nil {
+		in, out := &in.LogServersPerDisk, &out.LogServersPerDisk
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
 	if in.ImageTypes != nil {
 		in, out := &in.ImageTypes, &out.ImageTypes
 		*out = make([]ImageType, len(*in))
diff --git a/api/v1beta2/foundationdb_process_class.go b/api/v1beta2/foundationdb_process_class.go
index 98f2789b..8d560803 100644
--- a/api/v1beta2/foundationdb_process_class.go
+++ b/api/v1beta2/foundationdb_process_class.go
@@ -20,6 +20,10 @@
 
 package v1beta2
 
+import (
+	"strings"
+)
+
 // ProcessClass models the class of a pod
 type ProcessClass string
 
@@ -57,3 +61,15 @@ func (pClass ProcessClass) IsStateful() bool {
 func (pClass ProcessClass) IsTransaction() bool {
 	return pClass != ProcessClassStorage && pClass != ProcessClassGeneral
 }
+
+// SupportsMultipleLogServers determines whether a process class supports multiple log servers. This includes the log
+// class and the transaction class.
+func (pClass ProcessClass) SupportsMultipleLogServers() bool {
+	return pClass == ProcessClassLog || pClass == ProcessClassTransaction
+}
+
+// GetServersPerPodEnvName returns the environment variable name for the servers per Pod.
+// TODO (johscheuer): Revisit this decision: Shouldn't this be an annotation?
+func (pClass ProcessClass) GetServersPerPodEnvName() string {
+	return strings.ToUpper(string(pClass)) + "_SERVERS_PER_POD"
+}
diff --git a/api/v1beta2/foundationdbcluster_types.go b/api/v1beta2/foundationdbcluster_types.go
index f88a8a8b..d26cf91c 100644
--- a/api/v1beta2/foundationdbcluster_types.go
+++ b/api/v1beta2/foundationdbcluster_types.go
@@ -179,9 +179,16 @@ type FoundationDBClusterSpec struct {
 	// a single process group (Pod). This number defines the number of processes running
 	// in one Pod whereas the ProcessCounts defines the number of Pods created.
 	// This means that you end up with ProcessCounts["storage"] * StorageServersPerPod
-	// storage processes
+	// storage processes.
 	StorageServersPerPod int `json:"storageServersPerPod,omitempty"`
 
+	// LogServersPerPod defines how many Log Servers should run in
+	// a single process group (Pod). This number defines the number of processes running
+	// in one Pod whereas the ProcessCounts defines the number of Pods created.
+	// This means that you end up with ProcessCounts["Log"] * LogServersPerPod
+	// log processes. This also affects processes with the transaction class.
+	LogServersPerPod int `json:"logServersPerPod,omitempty"`
+
 	// MinimumUptimeSecondsForBounce defines the minimum time, in seconds, that the
 	// processes in the cluster must have been up for before the operator can
 	// execute a bounce.
@@ -275,6 +282,10 @@ type FoundationDBClusterStatus struct {
 	// If there are more than one value in the slice the reconcile phase is not finished.
 	StorageServersPerDisk []int `json:"storageServersPerDisk,omitempty"`
 
+	// LogServersPerDisk defines the LogServersPerDisk observed in the cluster.
+	// If there are more than one value in the slice the reconcile phase is not finished.
+	LogServersPerDisk []int `json:"logServersPerDisk,omitempty"`
+
 	// ImageTypes defines the kinds of images that are in use in the cluster.
 	// If there is more than one value in the slice the reconcile phase is not
 	// finished.
@@ -1500,6 +1511,19 @@ func (cluster *FoundationDBCluster) CheckReconciliation(log logr.Logger) (bool,
 	return reconciled, nil
 }
 
+// GetDesiredServersPerPod will return the expected server per Pod for the provided process class.
+func (cluster *FoundationDBCluster) GetDesiredServersPerPod(pClass ProcessClass) int {
+	if pClass == ProcessClassStorage {
+		return cluster.GetStorageServersPerPod()
+	}
+
+	if pClass.SupportsMultipleLogServers() {
+		return cluster.GetLogServersPerPod()
+	}
+
+	return 1
+}
+
 // GetStorageServersPerPod returns the StorageServer per Pod.
 func (cluster *FoundationDBCluster) GetStorageServersPerPod() int {
 	if cluster.Spec.StorageServersPerPod <= 1 {
@@ -1509,6 +1533,15 @@ func (cluster *FoundationDBCluster) GetStorageServersPerPod() int {
 	return cluster.Spec.StorageServersPerPod
 }
 
+// GetLogServersPerPod returns the TLog processes per Pod.
+func (cluster *FoundationDBCluster) GetLogServersPerPod() int {
+	if cluster.Spec.LogServersPerPod <= 1 {
+		return 1
+	}
+
+	return cluster.Spec.LogServersPerPod
+}
+
 // alphanum provides the characters that are used for the generation ID in the
 // connection string.
 var alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@@ -1993,15 +2026,26 @@ const (
 	PublicIPSourceService PublicIPSource = "service"
 )
 
-// AddStorageServerPerDisk adds serverPerDisk to the status field to keep track which ConfigMaps should be kept
-func (clusterStatus *FoundationDBClusterStatus) AddStorageServerPerDisk(serversPerDisk int) {
-	for _, curServersPerDisk := range clusterStatus.StorageServersPerDisk {
-		if curServersPerDisk == serversPerDisk {
-			return
+// AddServersPerDisk adds serverPerDisk to the status field to keep track which ConfigMaps should be kept
+func (clusterStatus *FoundationDBClusterStatus) AddServersPerDisk(serversPerDisk int, pClass ProcessClass) {
+	if pClass == ProcessClassStorage {
+		for _, curServersPerDisk := range clusterStatus.StorageServersPerDisk {
+			if curServersPerDisk == serversPerDisk {
+				return
+			}
 		}
+		clusterStatus.StorageServersPerDisk = append(clusterStatus.StorageServersPerDisk, serversPerDisk)
+		return
 	}
 
-	clusterStatus.StorageServersPerDisk = append(clusterStatus.StorageServersPerDisk, serversPerDisk)
+	if pClass.SupportsMultipleLogServers() {
+		for _, curServersPerDisk := range clusterStatus.LogServersPerDisk {
+			if curServersPerDisk == serversPerDisk {
+				return
+			}
+		}
+		clusterStatus.LogServersPerDisk = append(clusterStatus.LogServersPerDisk, serversPerDisk)
+	}
 }
 
 // GetMaxConcurrentAutomaticReplacements returns the cluster setting for MaxConcurrentReplacements, defaults to 1 if unset.
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index 555fb880..df63320e 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -729,6 +729,11 @@ func (in *FoundationDBClusterStatus) DeepCopyInto(out *FoundationDBClusterStatus
 		*out = make([]int, len(*in))
 		copy(*out, *in)
 	}
+	if in.LogServersPerDisk != nil {
+		in, out := &in.LogServersPerDisk, &out.LogServersPerDisk
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
 	if in.ImageTypes != nil {
 		in, out := &in.ImageTypes, &out.ImageTypes
 		*out = make([]ImageType, len(*in))
diff --git a/config/crd/bases/apps.foundationdb.org_foundationdbclusters.yaml b/config/crd/bases/apps.foundationdb.org_foundationdbclusters.yaml
index 1214825b..f54e08c6 100644
--- a/config/crd/bases/apps.foundationdb.org_foundationdbclusters.yaml
+++ b/config/crd/bases/apps.foundationdb.org_foundationdbclusters.yaml
@@ -9966,6 +9966,10 @@ spec:
                       type: string
                     type: array
                 type: object
+              logServersPerDisk:
+                items:
+                  type: integer
+                type: array
               missingProcesses:
                 additionalProperties:
                   format: int64
@@ -10465,6 +10469,8 @@ spec:
                 type: object
               logGroup:
                 type: string
+              logServersPerPod:
+                type: integer
               mainContainer:
                 properties:
                   enableLivenessProbe:
@@ -14217,6 +14223,10 @@ spec:
                       type: string
                     type: array
                 type: object
+              logServersPerDisk:
+                items:
+                  type: integer
+                type: array
               maintenanceModeInfo:
                 properties:
                   processGroups:
diff --git a/controllers/add_pods.go b/controllers/add_pods.go
index 13a097f1..de8278eb 100644
--- a/controllers/add_pods.go
+++ b/controllers/add_pods.go
@@ -90,7 +90,7 @@ func (a addPods) reconcile(ctx context.Context, r *FoundationDBClusterReconciler
 			return &requeue{curError: err}
 		}
 
-		serverPerPod, err := internal.GetStorageServersPerPodForPod(pod)
+		serverPerPod, err := internal.GetServersPerPodForPod(pod, processGroup.ProcessClass)
 		if err != nil {
 			return &requeue{curError: err}
 		}
diff --git a/controllers/bounce_processes.go b/controllers/bounce_processes.go
index ec12cedf..86f7f90d 100644
--- a/controllers/bounce_processes.go
+++ b/controllers/bounce_processes.go
@@ -263,6 +263,11 @@ func getProcessesReadyForRestart(logger logr.Logger, cluster *fdbv1beta2.Foundat
 		expectedProcesses += counts.Storage * (cluster.Spec.StorageServersPerPod - 1)
 	}
 
+	if cluster.Spec.LogServersPerPod > 1 {
+		expectedProcesses += counts.Log * (cluster.Spec.LogServersPerPod - 1)
+		expectedProcesses += counts.Transaction * (cluster.Spec.LogServersPerPod - 1)
+	}
+
 	if cluster.IsBeingUpgradedWithVersionIncompatibleVersion() && expectedProcesses != len(addresses) {
 		return nil, &requeue{
 			message:        fmt.Sprintf("expected %d processes, got %d processes ready to restart", expectedProcesses, len(addresses)),
diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go
index 95bc6c64..35561e16 100644
--- a/controllers/cluster_controller.go
+++ b/controllers/cluster_controller.go
@@ -293,18 +293,14 @@ func (r *FoundationDBClusterReconciler) updatePodDynamicConf(logger logr.Logger,
 		return false, nil
 	}
 
-	serversPerPod := 1
-
 	processClass, err := podmanager.GetProcessClass(cluster, pod)
 	if err != nil {
 		return false, err
 	}
 
-	if processClass == fdbv1beta2.ProcessClassStorage {
-		serversPerPod, err = internal.GetStorageServersPerPodForPod(pod)
-		if err != nil {
-			return false, err
-		}
+	serversPerPod, err := internal.GetServersPerPodForPod(pod, processClass)
+	if err != nil {
+		return false, err
 	}
 
 	var expectedConf string
diff --git a/controllers/update_pod_config.go b/controllers/update_pod_config.go
index bc8bbed4..e73225a6 100644
--- a/controllers/update_pod_config.go
+++ b/controllers/update_pod_config.go
@@ -71,7 +71,7 @@ func (updatePodConfig) reconcile(ctx context.Context, r *FoundationDBClusterReco
 			continue
 		}
 
-		serverPerPod, err := internal.GetStorageServersPerPodForPod(pod)
+		serverPerPod, err := internal.GetServersPerPodForPod(pod, processGroup.ProcessClass)
 		if err != nil {
 			curLogger.Error(err, "Error when receiving storage server per Pod")
 			errs = append(errs, err)
diff --git a/controllers/update_status.go b/controllers/update_status.go
index 5c26025f..6e9e0dc9 100644
--- a/controllers/update_status.go
+++ b/controllers/update_status.go
@@ -58,6 +58,7 @@ func (updateStatus) reconcile(ctx context.Context, r *FoundationDBClusterReconci
 
 	// Initialize with the current desired storage servers per Pod
 	clusterStatus.StorageServersPerDisk = []int{cluster.GetStorageServersPerPod()}
+	clusterStatus.LogServersPerDisk = []int{cluster.GetLogServersPerPod()}
 	clusterStatus.ImageTypes = []fdbv1beta2.ImageType{fdbv1beta2.ImageType(internal.GetDesiredImageType(cluster))}
 
 	processMap := make(map[fdbv1beta2.ProcessGroupID][]fdbv1beta2.FoundationDBStatusProcessInfo)
@@ -234,6 +235,7 @@ func (updateStatus) reconcile(ctx context.Context, r *FoundationDBClusterReconci
 	// Sort slices that are assembled based on pods to prevent a reordering from
 	// issuing a new reconcile loop.
 	sort.Ints(clusterStatus.StorageServersPerDisk)
+	sort.Ints(clusterStatus.LogServersPerDisk)
 	sort.Slice(clusterStatus.ImageTypes, func(i int, j int) bool {
 		return string(clusterStatus.ImageTypes[i]) < string(clusterStatus.ImageTypes[j])
 	})
@@ -385,7 +387,6 @@ func checkAndSetProcessStatus(logger logr.Logger, r *FoundationDBClusterReconcil
 
 // Validate and set progressGroup's status
 func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler, cluster *fdbv1beta2.FoundationDBCluster, status *fdbv1beta2.FoundationDBClusterStatus, processMap map[fdbv1beta2.ProcessGroupID][]fdbv1beta2.FoundationDBStatusProcessInfo, configMap *corev1.ConfigMap, pvcs *corev1.PersistentVolumeClaimList, logger logr.Logger) ([]*fdbv1beta2.ProcessGroupStatus, error) {
-	var err error
 	processGroups := status.ProcessGroups
 	processGroupsWithoutExclusion := make(map[fdbv1beta2.ProcessGroupID]fdbv1beta2.None, len(cluster.Spec.ProcessGroupsToRemoveWithoutExclusion))
 
@@ -430,7 +431,6 @@ func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler
 		}
 
 		processGroup.AddAddresses(podmanager.GetPublicIPs(pod, logger), processGroup.IsMarkedForRemoval() || !status.Health.Available)
-		processCount := 1
 
 		// In this case the Pod has a DeletionTimestamp and should be deleted.
 		if !pod.ObjectMeta.DeletionTimestamp.IsZero() {
@@ -453,14 +453,11 @@ func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler
 
 		// Even the process group will be removed we need to keep the config around.
 		// Set the processCount for the process group specific storage servers per pod
-		if processGroup.ProcessClass == fdbv1beta2.ProcessClassStorage {
-			processCount, err = internal.GetStorageServersPerPodForPod(pod)
-			if err != nil {
-				return processGroups, err
-			}
-
-			status.AddStorageServerPerDisk(processCount)
+		processCount, err := internal.GetServersPerPodForPod(pod, processGroup.ProcessClass)
+		if err != nil {
+			return processGroups, err
 		}
+		status.AddServersPerDisk(processCount, processGroup.ProcessClass)
 
 		imageType := internal.GetImageType(pod)
 		imageTypeString := fdbv1beta2.ImageType(imageType)
diff --git a/docs/cluster_spec.md b/docs/cluster_spec.md
index 4a56bb58..ded72b5c 100644
--- a/docs/cluster_spec.md
+++ b/docs/cluster_spec.md
@@ -252,7 +252,8 @@ FoundationDBClusterSpec defines the desired state of a cluster.
 | routing | Routing defines the configuration for routing to our pods. | [RoutingConfig](#routingconfig) | false |
 | ignoreUpgradabilityChecks | IgnoreUpgradabilityChecks determines whether we should skip the check for client compatibility when performing an upgrade. | bool | false |
 | buggify | Buggify defines settings for injecting faults into a cluster for testing. | [BuggifyConfig](#buggifyconfig) | false |
-| storageServersPerPod | StorageServersPerPod defines how many Storage Servers should run in a single process group (Pod). This number defines the number of processes running in one Pod whereas the ProcessCounts defines the number of Pods created. This means that you end up with ProcessCounts[\"storage\"] * StorageServersPerPod storage processes | int | false |
+| storageServersPerPod | StorageServersPerPod defines how many Storage Servers should run in a single process group (Pod). This number defines the number of processes running in one Pod whereas the ProcessCounts defines the number of Pods created. This means that you end up with ProcessCounts[\"storage\"] * StorageServersPerPod storage processes. | int | false |
+| logServersPerPod | LogServersPerPod defines how many Log Servers should run in a single process group (Pod). This number defines the number of processes running in one Pod whereas the ProcessCounts defines the number of Pods created. This means that you end up with ProcessCounts[\"Log\"] * LogServersPerPod log processes. This also affects processes with the transaction class. | int | false |
 | minimumUptimeSecondsForBounce | MinimumUptimeSecondsForBounce defines the minimum time, in seconds, that the processes in the cluster must have been up for before the operator can execute a bounce. | int | false |
 | replaceInstancesWhenResourcesChange | ReplaceInstancesWhenResourcesChange defines if an instance should be replaced when the resource requirements are increased. This can be useful with the combination of local storage. | *bool | false |
 | skip | Skip defines if the cluster should be skipped for reconciliation. This can be useful for investigating in issues or if the environment is unstable. | bool | false |
@@ -282,6 +283,7 @@ FoundationDBClusterStatus defines the observed state of FoundationDBCluster
 | configured | Configured defines whether we have configured the database yet. | bool | false |
 | hasListenIPsForAllPods | HasListenIPsForAllPods defines whether every pod has an environment variable for its listen address. | bool | false |
 | storageServersPerDisk | StorageServersPerDisk defines the storageServersPerPod observed in the cluster. If there are more than one value in the slice the reconcile phase is not finished. | []int | false |
+| logServersPerDisk | LogServersPerDisk defines the LogServersPerDisk observed in the cluster. If there are more than one value in the slice the reconcile phase is not finished. | []int | false |
 | imageTypes | ImageTypes defines the kinds of images that are in use in the cluster. If there is more than one value in the slice the reconcile phase is not finished. | [][ImageType](#imagetype) | false |
 | processGroups | ProcessGroups contain information about a process group. This information is used in multiple places to trigger the according action. | []*[ProcessGroupStatus](#processgroupstatus) | false |
 | locks | Locks contains information about the locking system. | [LockSystemStatus](#locksystemstatus) | false |
diff --git a/internal/configmap_helper.go b/internal/configmap_helper.go
index 1ff5ed11..0b12cb11 100644
--- a/internal/configmap_helper.go
+++ b/internal/configmap_helper.go
@@ -66,59 +66,48 @@ func GetConfigMap(cluster *fdbv1beta2.FoundationDBCluster) (*corev1.ConfigMap, e
 		imageTypes[FDBImageType(imageType)] = fdbv1beta2.None{}
 	}
 
-	storageServersPerDisk := cluster.Status.StorageServersPerDisk
-	// If the status field is not initialized we fallback to only the specified count
-	// in the cluster spec. This should only happen in the initial phase of a new cluster.
-	if len(cluster.Status.StorageServersPerDisk) == 0 {
-		storageServersPerDisk = []int{cluster.GetStorageServersPerPod()}
-	}
-
 	for processClass, count := range desiredCounts {
 		if count == 0 {
 			continue
 		}
 
-		if _, useUnifiedImage := imageTypes[FDBImageTypeUnified]; useUnifiedImage {
-			if processClass == fdbv1beta2.ProcessClassStorage {
-				for _, serversPerPod := range storageServersPerDisk {
-					config, err := GetMonitorProcessConfiguration(cluster, processClass, serversPerPod, FDBImageTypeUnified, nil)
-					if err != nil {
-						return nil, err
-					}
-					jsonData, err := json.Marshal(config)
-					if err != nil {
-						return nil, err
-					}
-					filename := GetConfigMapMonitorConfEntry(processClass, FDBImageTypeUnified, serversPerPod)
-					data[filename] = string(jsonData)
-				}
+		serversPerPodSlice := []int{1}
+		if processClass == fdbv1beta2.ProcessClassStorage {
+			// If the status field is not initialized we fallback to only the specified count
+			// in the cluster spec. This should only happen in the initial phase of a new cluster.
+			if len(cluster.Status.StorageServersPerDisk) == 0 {
+				serversPerPodSlice = []int{cluster.GetDesiredServersPerPod(processClass)}
 			} else {
-				config, err := GetMonitorProcessConfiguration(cluster, processClass, 1, FDBImageTypeUnified, nil)
-				if err != nil {
-					return nil, err
-				}
-				jsonData, err := json.Marshal(config)
+				serversPerPodSlice = cluster.Status.StorageServersPerDisk
+			}
+		}
+
+		if processClass.SupportsMultipleLogServers() {
+			// If the status field is not initialized we fallback to only the specified count
+			// in the cluster spec. This should only happen in the initial phase of a new cluster.
+			if len(cluster.Status.LogServersPerDisk) == 0 {
+				serversPerPodSlice = []int{cluster.GetDesiredServersPerPod(processClass)}
+			} else {
+				serversPerPodSlice = cluster.Status.LogServersPerDisk
+			}
+		}
+
+		if _, useUnifiedImage := imageTypes[FDBImageTypeUnified]; useUnifiedImage {
+			for _, serversPerPod := range serversPerPodSlice {
+				filename, jsonData, err := getDataForMonitorConf(cluster, FDBImageTypeUnified, processClass, serversPerPod)
 				if err != nil {
 					return nil, err
 				}
-				data[fmt.Sprintf("fdbmonitor-conf-%s-json", processClass)] = string(jsonData)
+				data[filename] = string(jsonData)
 			}
 		}
 
 		if _, useSplitImage := imageTypes[FDBImageTypeSplit]; useSplitImage {
-			if processClass == fdbv1beta2.ProcessClassStorage {
-				for _, serversPerPod := range storageServersPerDisk {
-					err := setMonitorConfForFilename(cluster, data, GetConfigMapMonitorConfEntry(processClass, FDBImageTypeSplit, serversPerPod), connectionString, processClass, serversPerPod)
-					if err != nil {
-						return nil, err
-					}
+			for _, serversPerPod := range serversPerPodSlice {
+				err := setMonitorConfForFilename(cluster, data, GetConfigMapMonitorConfEntry(processClass, FDBImageTypeSplit, serversPerPod), connectionString, processClass, serversPerPod)
+				if err != nil {
+					return nil, err
 				}
-				continue
-			}
-
-			err := setMonitorConfForFilename(cluster, data, GetConfigMapMonitorConfEntry(processClass, FDBImageTypeSplit, 1), connectionString, processClass, 1)
-			if err != nil {
-				return nil, err
 			}
 		}
 	}
@@ -155,6 +144,19 @@ func getConfigMapMetadata(cluster *fdbv1beta2.FoundationDBCluster) metav1.Object
 	return metadata
 }
 
+func getDataForMonitorConf(cluster *fdbv1beta2.FoundationDBCluster, imageType FDBImageType, pClass fdbv1beta2.ProcessClass, serversPerPod int) (string, []byte, error) {
+	config, err := GetMonitorProcessConfiguration(cluster, pClass, serversPerPod, imageType, nil)
+	if err != nil {
+		return "", nil, err
+	}
+	jsonData, err := json.Marshal(config)
+	if err != nil {
+		return "", nil, err
+	}
+	filename := GetConfigMapMonitorConfEntry(pClass, imageType, serversPerPod)
+	return filename, jsonData, nil
+}
+
 func setMonitorConfForFilename(cluster *fdbv1beta2.FoundationDBCluster, data map[string]string, filename string, connectionString string, processClass fdbv1beta2.ProcessClass, serversPerPod int) error {
 	if connectionString == "" {
 		data[filename] = ""
@@ -172,17 +174,14 @@ func setMonitorConfForFilename(cluster *fdbv1beta2.FoundationDBCluster, data map
 // GetConfigMapMonitorConfEntry returns the specific key for the monitor conf in the ConfigMap
 func GetConfigMapMonitorConfEntry(pClass fdbv1beta2.ProcessClass, imageType FDBImageType, serversPerPod int) string {
 	if imageType == FDBImageTypeUnified {
-		if serversPerPod > 1 && pClass == fdbv1beta2.ProcessClassStorage {
+		if serversPerPod > 1 {
 			return fmt.Sprintf("fdbmonitor-conf-%s-json-multiple", pClass)
 		}
-
 		return fmt.Sprintf("fdbmonitor-conf-%s-json", pClass)
 	}
-
-	if serversPerPod > 1 && pClass == fdbv1beta2.ProcessClassStorage {
+	if serversPerPod > 1 {
 		return fmt.Sprintf("fdbmonitor-conf-%s-density-%d", pClass, serversPerPod)
 	}
-
 	return fmt.Sprintf("fdbmonitor-conf-%s", pClass)
 }
 
diff --git a/internal/pod_models.go b/internal/pod_models.go
index 4519e33a..37c8cfc4 100644
--- a/internal/pod_models.go
+++ b/internal/pod_models.go
@@ -200,10 +200,11 @@ func configureContainersForUnifiedImages(cluster *fdbv1beta2.FoundationDBCluster
 		"--log-path", "/var/log/fdb-trace-logs/monitor.log",
 	}
 
-	if cluster.Spec.StorageServersPerPod > 1 && processClass == fdbv1beta2.ProcessClassStorage {
-		storageServers := strconv.Itoa(cluster.Spec.StorageServersPerPod)
-		mainContainer.Args = append(mainContainer.Args, "--process-count", storageServers)
-		mainContainer.Env = append(mainContainer.Env, corev1.EnvVar{Name: "STORAGE_SERVERS_PER_POD", Value: storageServers})
+	serversPerPod := cluster.GetDesiredServersPerPod(processClass)
+	if serversPerPod > 1 {
+		desiredServersPerPod := strconv.Itoa(serversPerPod)
+		mainContainer.Args = append(mainContainer.Args, "--process-count", desiredServersPerPod)
+		mainContainer.Env = append(mainContainer.Env, corev1.EnvVar{Name: processClass.GetServersPerPodEnvName(), Value: desiredServersPerPod})
 	}
 
 	mainContainer.VolumeMounts = append(mainContainer.VolumeMounts,
@@ -308,7 +309,7 @@ func setAffinityForFaultDomain(cluster *fdbv1beta2.FoundationDBCluster, podSpec
 
 func configureVolumesForContainers(cluster *fdbv1beta2.FoundationDBCluster, podSpec *corev1.PodSpec, volumeClaimTemplate *corev1.PersistentVolumeClaim, podName string, processClass fdbv1beta2.ProcessClass) {
 	useUnifiedImages := pointer.BoolDeref(cluster.Spec.UseUnifiedImage, false)
-	monitorConfKey := GetConfigMapMonitorConfEntry(processClass, GetDesiredImageType(cluster), cluster.GetStorageServersPerPod())
+	monitorConfKey := GetConfigMapMonitorConfEntry(processClass, GetDesiredImageType(cluster), cluster.GetDesiredServersPerPod(processClass))
 
 	var monitorConfFile string
 	if useUnifiedImages {
@@ -476,8 +477,9 @@ func GetPodSpec(cluster *fdbv1beta2.FoundationDBCluster, processClass fdbv1beta2
 			return nil, err
 		}
 
-		if processClass == fdbv1beta2.ProcessClassStorage && cluster.GetStorageServersPerPod() > 1 {
-			sidecarContainer.Env = append(sidecarContainer.Env, corev1.EnvVar{Name: "STORAGE_SERVERS_PER_POD", Value: fmt.Sprintf("%d", cluster.GetStorageServersPerPod())})
+		serversPerPod := cluster.GetDesiredServersPerPod(processClass)
+		if serversPerPod > 1 {
+			sidecarContainer.Env = append(sidecarContainer.Env, corev1.EnvVar{Name: processClass.GetServersPerPodEnvName(), Value: strconv.Itoa(serversPerPod)})
 		}
 	}
 
@@ -973,23 +975,23 @@ func GetBackupDeployment(backup *fdbv1beta2.FoundationDBBackup) (*appsv1.Deploym
 	return deployment, nil
 }
 
-// GetStorageServersPerPodForPod returns the value of STORAGE_SERVERS_PER_POD from the sidecar or 1
-func GetStorageServersPerPodForPod(pod *corev1.Pod) (int, error) {
+// GetServersPerPodForPod returns the count of servers per Pod based on the processClass from the sidecar or 1
+func GetServersPerPodForPod(pod *corev1.Pod, pClass fdbv1beta2.ProcessClass) (int, error) {
 	// If not specified we will default to 1
-	storageServersPerPod := 1
+	serversPerPod := 1
 	if pod == nil {
-		return storageServersPerPod, nil
+		return serversPerPod, nil
 	}
 
 	for _, container := range pod.Spec.Containers {
 		for _, env := range container.Env {
-			if env.Name == "STORAGE_SERVERS_PER_POD" {
+			if env.Name == pClass.GetServersPerPodEnvName() {
 				return strconv.Atoi(env.Value)
 			}
 		}
 	}
 
-	return storageServersPerPod, nil
+	return serversPerPod, nil
 }
 
 // GetPodMetadata returns the metadata for a specific Pod
diff --git a/internal/replacements/replacements.go b/internal/replacements/replacements.go
index f6c6b919..6aec403e 100644
--- a/internal/replacements/replacements.go
+++ b/internal/replacements/replacements.go
@@ -172,19 +172,19 @@ func processGroupNeedsRemoval(cluster *fdbv1beta2.FoundationDBCluster, pod *core
 			"reason", fmt.Sprintf("publicIP source has changed from %s to %s", ipSource, cluster.GetPublicIPSource()))
 		return true, nil
 	}
+	serversPerPod, err := internal.GetServersPerPodForPod(pod, processGroupStatus.ProcessClass)
+	if err != nil {
+		return false, err
+	}
 
-	if processGroupStatus.ProcessClass == fdbv1beta2.ProcessClassStorage {
-		// Replace the process group if the storage servers differ
-		storageServersPerPod, err := internal.GetStorageServersPerPodForPod(pod)
-		if err != nil {
-			return false, err
-		}
-
-		if storageServersPerPod != cluster.GetStorageServersPerPod() {
-			logger.Info("Replace process group",
-				"reason", fmt.Sprintf("storageServersPerPod has changed from %d to %d", storageServersPerPod, cluster.GetStorageServersPerPod()))
-			return true, nil
-		}
+	desiredServersPerPod := cluster.GetDesiredServersPerPod(processGroupStatus.ProcessClass)
+	// Replace the process group if the expected servers differ from the desired servers
+	if serversPerPod != desiredServersPerPod {
+		logger.Info("Replace process group",
+			"serversPerPod", serversPerPod,
+			"desiredServersPerPod", desiredServersPerPod,
+			"reason", fmt.Sprintf("serversPerPod have changes from current: %d to desired: %d", serversPerPod, desiredServersPerPod))
+		return true, nil
 	}
 
 	expectedNodeSelector := cluster.GetProcessSettings(processGroupStatus.ProcessClass).PodTemplate.Spec.NodeSelector
diff --git a/pkg/fdbadminclient/mock/admin_client_mock.go b/pkg/fdbadminclient/mock/admin_client_mock.go
index b1e0c347..51a62cba 100644
--- a/pkg/fdbadminclient/mock/admin_client_mock.go
+++ b/pkg/fdbadminclient/mock/admin_client_mock.go
@@ -156,19 +156,22 @@ func (client *AdminClient) GetStatus() (*fdbv1beta2.FoundationDBStatus, error) {
 	}
 
 	for _, pod := range pods.Items {
-		podClient, _ := mock.NewMockFdbPodClient(client.Cluster, &pod)
+		processClass, err := podmanager.GetProcessClass(client.Cluster, &pod)
+		if err != nil {
+			return nil, err
+		}
 
-		processCount, err := internal.GetStorageServersPerPodForPod(&pod)
+		processCount, err := internal.GetServersPerPodForPod(&pod, processClass)
 		if err != nil {
 			return nil, err
 		}
 
 		processGroupID := podmanager.GetProcessGroupID(client.Cluster, &pod)
-
 		if _, ok := client.missingProcessGroups[processGroupID]; ok {
 			continue
 		}
 
+		podClient, _ := mock.NewMockFdbPodClient(client.Cluster, &pod)
 		subs, err := podClient.GetVariableSubstitutions()
 		if err != nil {
 			return nil, err


Test patch:
diff --git a/api/v1beta2/foundationdbcluster_types_test.go b/api/v1beta2/foundationdbcluster_types_test.go
index 7e2ca945..20c26999 100644
--- a/api/v1beta2/foundationdbcluster_types_test.go
+++ b/api/v1beta2/foundationdbcluster_types_test.go
@@ -28,12 +28,11 @@ import (
 
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/pointer"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 )
 
 var _ = Describe("[api] FoundationDBCluster", func() {
@@ -3626,7 +3625,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 				}
 
 				for _, val := range tc.ValuesToAdd {
-					status.AddStorageServerPerDisk(val)
+					status.AddServersPerDisk(val, ProcessClassStorage)
 				}
 
 				Expect(len(status.StorageServersPerDisk)).To(BeNumerically("==", tc.ExpectedLen))
@@ -3653,6 +3652,47 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 		)
 	})
 
+	When("adding LogServersPerDisk", func() {
+		type testCase struct {
+			ValuesToAdd               []int
+			ExpectedLen               int
+			ExpectedLogServersPerDisk []int
+		}
+
+		DescribeTable("should generate the status correctly",
+			func(tc testCase) {
+				status := FoundationDBClusterStatus{
+					LogServersPerDisk: []int{},
+				}
+
+				for _, val := range tc.ValuesToAdd {
+					status.AddServersPerDisk(val, ProcessClassLog)
+				}
+
+				Expect(len(status.LogServersPerDisk)).To(BeNumerically("==", tc.ExpectedLen))
+				Expect(status.LogServersPerDisk).To(Equal(tc.ExpectedLogServersPerDisk))
+			},
+			Entry("Add missing element",
+				testCase{
+					ValuesToAdd:               []int{1},
+					ExpectedLen:               1,
+					ExpectedLogServersPerDisk: []int{1},
+				}),
+			Entry("Duplicates should only inserted once",
+				testCase{
+					ValuesToAdd:               []int{1, 1},
+					ExpectedLen:               1,
+					ExpectedLogServersPerDisk: []int{1},
+				}),
+			Entry("Multiple elements should be added",
+				testCase{
+					ValuesToAdd:               []int{1, 2},
+					ExpectedLen:               2,
+					ExpectedLogServersPerDisk: []int{1, 2},
+				}),
+		)
+	})
+
 	When("adding addresses to a process group", func() {
 		type testCase struct {
 			initialProcessGroup  ProcessGroupStatus
diff --git a/controllers/bounce_processes_test.go b/controllers/bounce_processes_test.go
index 1fed94f4..bbf40503 100644
--- a/controllers/bounce_processes_test.go
+++ b/controllers/bounce_processes_test.go
@@ -285,6 +285,84 @@ var _ = Describe("bounceProcesses", func() {
 		})
 	})
 
+	Context("with multiple log servers per pod", func() {
+		BeforeEach(func() {
+			cluster.Spec.LogServersPerPod = 2
+			Expect(k8sClient.Update(context.TODO(), cluster)).NotTo(HaveOccurred())
+			result, err := reconcileCluster(cluster)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(result.Requeue).To(BeFalse())
+			_, err = reloadCluster(cluster)
+			Expect(err).NotTo(HaveOccurred())
+
+			for idx, processGroup := range cluster.Status.ProcessGroups {
+				if processGroup.ProcessGroupID != "log-5" && processGroup.ProcessGroupID != "log-6" {
+					continue
+				}
+
+				cluster.Status.ProcessGroups[idx].UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
+			}
+		})
+
+		It("should not requeue", func() {
+			Expect(requeue).To(BeNil())
+		})
+
+		It("should kill the targeted processes", func() {
+			addresses := make(map[string]fdbv1beta2.None, 2)
+			for _, processGroupID := range []fdbv1beta2.ProcessGroupID{"log-5", "log-6"} {
+				processGroupAddresses := fdbv1beta2.FindProcessGroupByID(cluster.Status.ProcessGroups, processGroupID).Addresses
+				for _, address := range processGroupAddresses {
+					addresses[fmt.Sprintf("%s:4501", address)] = fdbv1beta2.None{}
+					addresses[fmt.Sprintf("%s:4503", address)] = fdbv1beta2.None{}
+				}
+			}
+			Expect(adminClient.KilledAddresses).To(Equal(addresses))
+		})
+
+		When("doing an upgrade", func() {
+			BeforeEach(func() {
+				cluster.Spec.Version = fdbv1beta2.Versions.NextMajorVersion.String()
+				for _, processGroup := range cluster.Status.ProcessGroups {
+					processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
+				}
+			})
+
+			It("should requeue", func() {
+				Expect(requeue).NotTo(BeNil())
+			})
+
+			It("should kill all the processes", func() {
+				addresses := make(map[string]fdbv1beta2.None, len(cluster.Status.ProcessGroups))
+				for _, processGroup := range cluster.Status.ProcessGroups {
+					for _, address := range processGroup.Addresses {
+						addresses[fmt.Sprintf("%s:4501", address)] = fdbv1beta2.None{}
+						if processGroup.ProcessClass == fdbv1beta2.ProcessClassLog {
+							addresses[fmt.Sprintf("%s:4503", address)] = fdbv1beta2.None{}
+						}
+					}
+				}
+				Expect(adminClient.KilledAddresses).To(Equal(addresses))
+			})
+
+			It("should update the running version in the status", func() {
+				_, err = reloadCluster(cluster)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(cluster.Status.RunningVersion).To(Equal(fdbv1beta2.Versions.NextMajorVersion.String()))
+			})
+
+			It("should submit pending upgrade information for all the processes", func() {
+				expectedUpgrades := make(map[fdbv1beta2.ProcessGroupID]bool, len(cluster.Status.ProcessGroups))
+				for _, processGroup := range cluster.Status.ProcessGroups {
+					expectedUpgrades[processGroup.ProcessGroupID] = true
+				}
+				pendingUpgrades, err := lockClient.GetPendingUpgrades(fdbv1beta2.Versions.NextMajorVersion)
+				Expect(err).NotTo(HaveOccurred())
+				Expect(pendingUpgrades).To(Equal(expectedUpgrades))
+			})
+		})
+	})
+
 	Context("with a pending upgrade", func() {
 		BeforeEach(func() {
 			cluster.Spec.Version = fdbv1beta2.Versions.NextMajorVersion.String()
diff --git a/controllers/cluster_controller_test.go b/controllers/cluster_controller_test.go
index af299d98..98e40ee8 100644
--- a/controllers/cluster_controller_test.go
+++ b/controllers/cluster_controller_test.go
@@ -1088,6 +1088,7 @@ var _ = Describe("cluster_controller", func() {
 					adminClient.UnfreezeStatus()
 					Expect(err).NotTo(HaveOccurred())
 					err = k8sClient.Update(context.TODO(), cluster)
+					Expect(err).NotTo(HaveOccurred())
 
 					result, err := reconcileCluster(cluster)
 					Expect(err).NotTo(HaveOccurred())
@@ -1124,6 +1125,51 @@ var _ = Describe("cluster_controller", func() {
 					Expect(adminClient.KilledAddresses).To(Equal(addresses))
 				})
 			})
+
+			Context("with multiple log servers per pod", func() {
+				BeforeEach(func() {
+					cluster.Spec.Processes = map[fdbv1beta2.ProcessClass]fdbv1beta2.ProcessSettings{fdbv1beta2.ProcessClassGeneral: {CustomParameters: fdbv1beta2.FoundationDBCustomParameters{}}}
+					cluster.Spec.LogServersPerPod = 2
+					adminClient.UnfreezeStatus()
+					Expect(err).NotTo(HaveOccurred())
+					err = k8sClient.Update(context.TODO(), cluster)
+					Expect(err).NotTo(HaveOccurred())
+
+					result, err := reconcileCluster(cluster)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(result.Requeue).To(BeFalse())
+
+					generation, err := reloadCluster(cluster)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(generation).To(Equal(originalVersion + generationGap))
+					originalVersion = cluster.ObjectMeta.Generation
+
+					err = k8sClient.List(context.TODO(), originalPods, getListOptions(cluster)...)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(len(originalPods.Items)).To(Equal(17))
+
+					sortPodsByName(originalPods)
+
+					err = adminClient.FreezeStatus()
+					Expect(err).NotTo(HaveOccurred())
+
+					cluster.Spec.Processes = map[fdbv1beta2.ProcessClass]fdbv1beta2.ProcessSettings{fdbv1beta2.ProcessClassGeneral: {CustomParameters: fdbv1beta2.FoundationDBCustomParameters{"knob_disable_posix_kernel_aio=1"}}}
+					err = k8sClient.Update(context.TODO(), cluster)
+					Expect(err).NotTo(HaveOccurred())
+				})
+
+				It("should bounce the processes", func() {
+					addresses := make(map[string]fdbv1beta2.None, len(originalPods.Items))
+					for _, pod := range originalPods.Items {
+						addresses[cluster.GetFullAddress(pod.Status.PodIP, 1).String()] = fdbv1beta2.None{}
+						if internal.ProcessClassFromLabels(cluster, pod.ObjectMeta.Labels) == fdbv1beta2.ProcessClassLog {
+							addresses[cluster.GetFullAddress(pod.Status.PodIP, 2).String()] = fdbv1beta2.None{}
+						}
+					}
+
+					Expect(adminClient.KilledAddresses).To(Equal(addresses))
+				})
+			})
 		})
 
 		Context("with a configuration change", func() {
@@ -3107,6 +3153,48 @@ var _ = Describe("cluster_controller", func() {
 			})
 		})
 
+		Context("with a basic log process group with multiple log servers per Pod", func() {
+			BeforeEach(func() {
+				cluster.Spec.LogServersPerPod = 2
+				conf, err = internal.GetMonitorConf(cluster, fdbv1beta2.ProcessClassLog, nil, cluster.GetLogServersPerPod())
+				Expect(err).NotTo(HaveOccurred())
+			})
+
+			It("should generate the log conf with two processes", func() {
+				Expect(conf).To(Equal(strings.Join([]string{
+					"[general]",
+					"kill_on_configuration_change = false",
+					"restart_delay = 60",
+					"[fdbserver.1]",
+					"command = $BINARY_DIR/fdbserver",
+					"cluster_file = /var/fdb/data/fdb.cluster",
+					"seed_cluster_file = /var/dynamic-conf/fdb.cluster",
+					"public_address = $FDB_PUBLIC_IP:4501",
+					"class = log",
+					"logdir = /var/log/fdb-trace-logs",
+					"loggroup = " + cluster.Name,
+					"datadir = /var/fdb/data/1",
+					"locality_process_id = $FDB_INSTANCE_ID-1",
+					"locality_instance_id = $FDB_INSTANCE_ID",
+					"locality_machineid = $FDB_MACHINE_ID",
+					"locality_zoneid = $FDB_ZONE_ID",
+					"[fdbserver.2]",
+					"command = $BINARY_DIR/fdbserver",
+					"cluster_file = /var/fdb/data/fdb.cluster",
+					"seed_cluster_file = /var/dynamic-conf/fdb.cluster",
+					"public_address = $FDB_PUBLIC_IP:4503",
+					"class = log",
+					"logdir = /var/log/fdb-trace-logs",
+					"loggroup = " + cluster.Name,
+					"datadir = /var/fdb/data/2",
+					"locality_process_id = $FDB_INSTANCE_ID-2",
+					"locality_instance_id = $FDB_INSTANCE_ID",
+					"locality_machineid = $FDB_MACHINE_ID",
+					"locality_zoneid = $FDB_ZONE_ID",
+				}, "\n")))
+			})
+		})
+
 		Context("with the public IP from the pod", func() {
 			BeforeEach(func() {
 				source := fdbv1beta2.PublicIPSourcePod
@@ -3677,7 +3765,7 @@ func getConfigMapHash(cluster *fdbv1beta2.FoundationDBCluster, pClass fdbv1beta2
 		return "", err
 	}
 
-	serversPerPod, err := internal.GetStorageServersPerPodForPod(pod)
+	serversPerPod, err := internal.GetServersPerPodForPod(pod, pClass)
 	if err != nil {
 		return "", err
 	}
diff --git a/e2e/fixtures/cluster_config.go b/e2e/fixtures/cluster_config.go
index bba85595..288c77e8 100644
--- a/e2e/fixtures/cluster_config.go
+++ b/e2e/fixtures/cluster_config.go
@@ -79,6 +79,8 @@ type ClusterConfig struct {
 	DisksPerMachine int
 	// StorageServerPerPod defines the value that is set in the FoundationDBClusterSpec for this setting.
 	StorageServerPerPod int
+	// LogServersPerPod defines the value that is set in the FoundationDBClusterSpec for this setting.
+	LogServersPerPod int
 	// VolumeSize the size of the volumes that should be created for stateful Pods.
 	VolumeSize string
 	// Namespace to create the cluster in, if empty will use a randomly generated namespace. The setup won't create the
@@ -203,6 +205,11 @@ func (config *ClusterConfig) generatePodResources(
 		memory *= config.StorageServerPerPod
 	}
 
+	if processClass == fdbv1beta2.ProcessClassLog && config.LogServersPerPod > 1 {
+		cpu *= config.LogServersPerPod
+		memory *= config.LogServersPerPod
+	}
+
 	return corev1.ResourceList{
 		corev1.ResourceCPU:    resource.MustParse(strconv.Itoa(cpu)),
 		corev1.ResourceMemory: resource.MustParse(fmt.Sprintf("%dGi", memory)),
diff --git a/e2e/fixtures/fdb_cluster.go b/e2e/fixtures/fdb_cluster.go
index c7c49d70..a04ed769 100644
--- a/e2e/fixtures/fdb_cluster.go
+++ b/e2e/fixtures/fdb_cluster.go
@@ -504,6 +504,11 @@ func (fdbCluster *FdbCluster) GetStoragePods() *corev1.PodList {
 	return fdbCluster.getPodsByProcessClass(fdbv1beta2.ProcessClassStorage)
 }
 
+// GetTransactionPods returns all Pods of this cluster that have the process class transaction.
+func (fdbCluster *FdbCluster) GetTransactionPods() *corev1.PodList {
+	return fdbCluster.getPodsByProcessClass(fdbv1beta2.ProcessClassTransaction)
+}
+
 // GetPod returns the Pod with the given name that runs in the same namespace as the FoundationDBCluster.
 func (fdbCluster *FdbCluster) GetPod(name string) *corev1.Pod {
 	pod := &corev1.Pod{}
@@ -550,6 +555,25 @@ func (fdbCluster *FdbCluster) GetVolumeClaimsForProcesses(
 	return volumeClaimList
 }
 
+// GetLogServersPerPod returns the current expected Log server per pod.
+func (fdbCluster *FdbCluster) GetLogServersPerPod() int {
+	return fdbCluster.cluster.GetLogServersPerPod()
+}
+
+// SetLogServersPerPod set the LogServersPerPod field in the cluster spec.
+func (fdbCluster *FdbCluster) SetLogServersPerPod(
+	serverPerPod int,
+	waitForReconcile bool,
+) error {
+	fdbCluster.cluster.Spec.LogServersPerPod = serverPerPod
+	fdbCluster.UpdateClusterSpec()
+
+	if !waitForReconcile {
+		return nil
+	}
+	return fdbCluster.WaitForReconciliation()
+}
+
 // GetStorageServerPerPod returns the current expected storage server per pod.
 func (fdbCluster *FdbCluster) GetStorageServerPerPod() int {
 	return fdbCluster.cluster.GetStorageServersPerPod()
@@ -573,6 +597,23 @@ func (fdbCluster *FdbCluster) SetStorageServerPerPod(serverPerPod int) error {
 	return fdbCluster.setStorageServerPerPod(serverPerPod, true)
 }
 
+// SetTransactionServerPerPod set the LogServersPerPod field in the cluster spec and changes log Pods to transaction Pods.
+func (fdbCluster *FdbCluster) SetTransactionServerPerPod(
+	serverPerPod int,
+	processCount int,
+	waitForReconcile bool,
+) error {
+	fdbCluster.cluster.Spec.LogServersPerPod = serverPerPod
+	fdbCluster.cluster.Spec.ProcessCounts.Transaction = processCount
+	fdbCluster.cluster.Spec.ProcessCounts.Log = 0
+	fdbCluster.UpdateClusterSpec()
+
+	if !waitForReconcile {
+		return nil
+	}
+	return fdbCluster.WaitForReconciliation()
+}
+
 // ReplacePod replaces the provided Pod if it's part of the FoundationDBCluster.
 func (fdbCluster *FdbCluster) ReplacePod(pod corev1.Pod, waitForReconcile bool) {
 	fdbCluster.cluster.Spec.ProcessGroupsToRemove = []fdbv1beta2.ProcessGroupID{GetProcessGroupID(pod)}
diff --git a/e2e/fixtures/fdb_cluster_specs.go b/e2e/fixtures/fdb_cluster_specs.go
index 52c837be..d8db9a3f 100644
--- a/e2e/fixtures/fdb_cluster_specs.go
+++ b/e2e/fixtures/fdb_cluster_specs.go
@@ -38,6 +38,7 @@ func (factory *Factory) GenerateFDBClusterSpec(config *ClusterConfig) *fdbv1beta
 		factory.createProcesses(config),
 		config.CreateDatabaseConfiguration(),
 		config.StorageServerPerPod,
+		config.LogServersPerPod,
 		mainOverrides,
 		sidecarOverrides)
 }
@@ -49,6 +50,7 @@ func (factory *Factory) createFDBClusterSpec(
 	processes map[fdbv1beta2.ProcessClass]fdbv1beta2.ProcessSettings,
 	databaseConfiguration fdbv1beta2.DatabaseConfiguration,
 	storageServersPerPod int,
+	logServersPerPod int,
 	mainContainerOverrides fdbv1beta2.ContainerOverrides,
 	sidecarContainerOverrides fdbv1beta2.ContainerOverrides,
 ) *fdbv1beta2.FoundationDBCluster {
@@ -63,6 +65,7 @@ func (factory *Factory) createFDBClusterSpec(
 			Processes:                     processes,
 			DatabaseConfiguration:         databaseConfiguration,
 			StorageServersPerPod:          storageServersPerPod,
+			LogServersPerPod:              logServersPerPod,
 			LogGroup:                      namespace + "-" + clusterName,
 			MainContainer:                 mainContainerOverrides,
 			SidecarContainer:              sidecarContainerOverrides,
diff --git a/e2e/fixtures/ha_fdb_cluster.go b/e2e/fixtures/ha_fdb_cluster.go
index a0d5badd..8d9f56b1 100644
--- a/e2e/fixtures/ha_fdb_cluster.go
+++ b/e2e/fixtures/ha_fdb_cluster.go
@@ -224,6 +224,7 @@ func (factory Factory) createHaFdbClusterSpec(
 		*databaseConfiguration,
 		// TODO(johscheuer): make this configurable.
 		1,
+		1,
 		mainContainerOverrides,
 		sidecarContainerOverrides,
 	)
diff --git a/e2e/fixtures/status.go b/e2e/fixtures/status.go
index 56589f40..a54e0cf6 100644
--- a/e2e/fixtures/status.go
+++ b/e2e/fixtures/status.go
@@ -307,6 +307,20 @@ func (fdbCluster *FdbCluster) GetProcessCount(targetRole fdbv1beta2.ProcessRole)
 	return pCounter
 }
 
+// GetProcessCountByProcessClass returns the number of processes based on process class
+func (fdbCluster *FdbCluster) GetProcessCountByProcessClass(pClass fdbv1beta2.ProcessClass) int {
+	pCounter := 0
+	status := fdbCluster.GetStatus()
+
+	for _, process := range status.Cluster.Processes {
+		if process.ProcessClass == pClass {
+			pCounter++
+		}
+	}
+
+	return pCounter
+}
+
 // HasTLSEnabled returns true if the cluster is running with TLS enabled.
 func (fdbCluster *FdbCluster) HasTLSEnabled() bool {
 	status := fdbCluster.GetStatus()
@@ -384,13 +398,17 @@ func (fdbCluster *FdbCluster) GetPodsWithRole(role fdbv1beta2.ProcessRole) []cor
 	var matches []corev1.Pod
 	for _, p := range pods.Items {
 		roles := roleMap[GetProcessGroupID(p)]
+		rolesAdded := map[string]fdbv1beta2.None{}
 		for _, r := range roles {
 			if r.Role == string(role) {
-				matches = append(matches, p)
+				_, ok := rolesAdded[r.Role]
+				if !ok {
+					rolesAdded[r.Role] = fdbv1beta2.None{}
+					matches = append(matches, p)
+				}
 			}
 		}
 	}
-
 	return matches
 }
 
diff --git a/e2e/test_operator/operator_test.go b/e2e/test_operator/operator_test.go
index d1f7f8c5..d77caf71 100644
--- a/e2e/test_operator/operator_test.go
+++ b/e2e/test_operator/operator_test.go
@@ -58,18 +58,36 @@ func init() {
 	testOptions = fixtures.InitFlags()
 }
 
-func validateStorageProcesses(
+func validateProcessesCount(
 	fdbCluster *fixtures.FdbCluster,
-	countStoragePods int,
-	countStorageServer int,
+	processRole fdbv1beta2.ProcessRole,
+	countPods int,
+	countServer int,
 ) {
 	// Using Eventually here to prevent some weird timing from the test runner
+	if processRole == fdbv1beta2.ProcessRoleStorage {
+		Eventually(func() int {
+			return len(fdbCluster.GetStoragePods().Items)
+		}).Should(BeNumerically("==", countPods))
+		Eventually(func() int {
+			return fdbCluster.GetProcessCount(processRole)
+		}).Should(BeNumerically("==", countServer))
+	} else if processRole == fdbv1beta2.ProcessRoleLog {
+		Eventually(func() int {
+			return len(fdbCluster.GetLogPods().Items)
+		}).Should(BeNumerically("==", countPods))
+	} else if processRole == fdbv1beta2.ProcessRole(fdbv1beta2.ProcessClassTransaction) {
+		Eventually(func() int {
+			return len(fdbCluster.GetTransactionPods().Items)
+		}).Should(BeNumerically("==", countPods))
+	} else {
+		Eventually(func() int {
+			return len(fdbCluster.GetPodsWithRole(processRole))
+		}).Should(BeNumerically("==", countPods))
+	}
 	Eventually(func() int {
-		return len(fdbCluster.GetStoragePods().Items)
-	}).Should(BeNumerically("==", countStoragePods))
-	Eventually(func() int {
-		return fdbCluster.GetProcessCount(fdbv1beta2.ProcessRoleStorage)
-	}).Should(BeNumerically("==", countStorageServer))
+		return fdbCluster.GetProcessCountByProcessClass(fdbv1beta2.ProcessClass(processRole))
+	}).Should(BeNumerically("==", countServer))
 }
 
 func validateStorageClass(processClass fdbv1beta2.ProcessClass, targetStorageClass string) {
@@ -394,7 +412,7 @@ var _ = Describe("Operator", Label("e2e", "pr"), func() {
 				expectedPodCnt,
 				expectedStorageProcessesCnt,
 			)
-			validateStorageProcesses(fdbCluster, expectedPodCnt, expectedStorageProcessesCnt)
+			validateProcessesCount(fdbCluster, fdbv1beta2.ProcessRoleStorage, expectedPodCnt, expectedStorageProcessesCnt)
 		})
 
 		AfterEach(func() {
@@ -405,8 +423,9 @@ var _ = Describe("Operator", Label("e2e", "pr"), func() {
 				expectedPodCnt,
 				expectedPodCnt*initialStorageServerPerPod,
 			)
-			validateStorageProcesses(
+			validateProcessesCount(
 				fdbCluster,
+				fdbv1beta2.ProcessRoleStorage,
 				expectedPodCnt,
 				expectedPodCnt*initialStorageServerPerPod,
 			)
@@ -422,7 +441,7 @@ var _ = Describe("Operator", Label("e2e", "pr"), func() {
 				expectedPodCnt,
 				expectedPodCnt*serverPerPod,
 			)
-			validateStorageProcesses(fdbCluster, expectedPodCnt, expectedPodCnt*serverPerPod)
+			validateProcessesCount(fdbCluster, fdbv1beta2.ProcessRoleStorage, expectedPodCnt, expectedPodCnt*serverPerPod)
 		})
 	})
 
@@ -892,6 +911,93 @@ var _ = Describe("Operator", Label("e2e", "pr"), func() {
 		})
 	})
 
+	When("setting 2 logs per disk", func() {
+		var initialLogServerPerPod, expectedPodCnt, expectedLogProcessesCnt int
+
+		BeforeEach(func() {
+			initialLogServerPerPod = fdbCluster.GetLogServersPerPod()
+			initialPods := fdbCluster.GetLogPods()
+			expectedPodCnt = len(initialPods.Items)
+			expectedLogProcessesCnt = expectedPodCnt * initialLogServerPerPod
+			log.Printf(
+				"expectedPodCnt: %d, expectedProcessesCnt: %d",
+				expectedPodCnt,
+				expectedLogProcessesCnt,
+			)
+			Eventually(func() int {
+				return len(fdbCluster.GetLogPods().Items)
+			}).Should(BeNumerically("==", expectedPodCnt))
+		})
+
+		AfterEach(func() {
+			log.Printf("set log servers per Pod to %d", initialLogServerPerPod)
+			Expect(fdbCluster.SetLogServersPerPod(initialLogServerPerPod, true)).ShouldNot(HaveOccurred())
+			log.Printf(
+				"expectedPodCnt: %d, expectedProcessesCnt: %d",
+				expectedPodCnt,
+				expectedPodCnt*initialLogServerPerPod,
+			)
+			Eventually(func() int {
+				return len(fdbCluster.GetLogPods().Items)
+			}).Should(BeNumerically("==", expectedPodCnt))
+		})
+
+		It("should update the log servers to the expected amount", func() {
+			serverPerPod := initialLogServerPerPod * 2
+			log.Printf("set log servers per Pod to %d", initialLogServerPerPod)
+			Expect(fdbCluster.SetLogServersPerPod(serverPerPod, true)).ShouldNot(HaveOccurred())
+			log.Printf(
+				"expectedPodCnt: %d, expectedStorageProcessesCnt: %d",
+				expectedPodCnt,
+				expectedPodCnt*serverPerPod,
+			)
+			validateProcessesCount(fdbCluster, fdbv1beta2.ProcessRoleLog, expectedPodCnt, expectedPodCnt*serverPerPod)
+		})
+	})
+
+	When("setting 2 logs per disk to use transaction process", func() {
+		var initialLogServerPerPod, expectedPodCnt, expectedLogProcessesCnt int
+
+		BeforeEach(func() {
+			initialLogServerPerPod = fdbCluster.GetLogServersPerPod()
+			initialPods := fdbCluster.GetLogPods()
+			expectedPodCnt = len(initialPods.Items)
+			expectedLogProcessesCnt = expectedPodCnt * initialLogServerPerPod
+			log.Printf(
+				"expectedPodCnt: %d, expectedProcessesCnt: %d",
+				expectedPodCnt,
+				expectedLogProcessesCnt,
+			)
+			Eventually(func() int {
+				return len(fdbCluster.GetLogPods().Items)
+			}).Should(BeNumerically("==", expectedPodCnt))
+		})
+
+		AfterEach(func() {
+			log.Printf("set log servers per Pod to %d", initialLogServerPerPod)
+			Expect(fdbCluster.SetTransactionServerPerPod(initialLogServerPerPod, expectedLogProcessesCnt, true)).ShouldNot(HaveOccurred())
+			log.Printf(
+				"expectedPodCnt: %d, expectedProcessesCnt: %d",
+				expectedPodCnt,
+				expectedPodCnt*initialLogServerPerPod,
+			)
+			Eventually(func() int {
+				return len(fdbCluster.GetLogPods().Items)
+			}).Should(BeNumerically("==", expectedPodCnt))
+		})
+
+		It("should update the log servers to the expected amount and should create transaction Pods", func() {
+			serverPerPod := initialLogServerPerPod * 2
+			Expect(fdbCluster.SetTransactionServerPerPod(serverPerPod, expectedLogProcessesCnt, true)).ShouldNot(HaveOccurred())
+			log.Printf(
+				"expectedPodCnt: %d, expectedProcessesCnt: %d",
+				expectedPodCnt,
+				expectedPodCnt*serverPerPod,
+			)
+			validateProcessesCount(fdbCluster, fdbv1beta2.ProcessRole(fdbv1beta2.ProcessClassTransaction), expectedPodCnt, expectedPodCnt*serverPerPod)
+		})
+	})
+
 	When("Migrating a cluster to a different storage class", func() {
 		var defaultStorageClass, targetStorageClass string
 
diff --git a/e2e/test_operator_upgrades/operator_upgrades_test.go b/e2e/test_operator_upgrades/operator_upgrades_test.go
index 8e8f8020..30bc4726 100644
--- a/e2e/test_operator_upgrades/operator_upgrades_test.go
+++ b/e2e/test_operator_upgrades/operator_upgrades_test.go
@@ -1135,4 +1135,60 @@ var _ = Describe("Operator Upgrades", Label("e2e", "pr"), func() {
 		EntryDescription("Upgrade from %[1]s to %[2]s"),
 		fixtures.GenerateUpgradeTableEntries(testOptions),
 	)
+
+	DescribeTable(
+		"with 2 log servers per Pod",
+		func(beforeVersion string, targetVersion string) {
+			clusterSetupWithConfig(beforeVersion, true, &fixtures.ClusterConfig{
+				DebugSymbols:     false,
+				LogServersPerPod: 2,
+			})
+
+			Expect(fdbCluster.UpgradeCluster(targetVersion, false)).NotTo(HaveOccurred())
+			// Make sure the cluster is still running with 2 log server per Pod.
+			Expect(fdbCluster.GetCluster().Spec.LogServersPerPod).To(Equal(2))
+
+			if !fixtures.VersionsAreProtocolCompatible(beforeVersion, targetVersion) {
+				// Ensure that the operator is setting the IncorrectConfigMap and IncorrectCommandLine conditions during the upgrade
+				// process.
+				expectedConditions := map[fdbv1beta2.ProcessGroupConditionType]bool{
+					fdbv1beta2.IncorrectConfigMap:   true,
+					fdbv1beta2.IncorrectCommandLine: true,
+				}
+				Eventually(func() bool {
+					cluster := fdbCluster.GetCluster()
+
+					for _, processGroup := range cluster.Status.ProcessGroups {
+						if !processGroup.MatchesConditions(expectedConditions) {
+							return false
+						}
+					}
+
+					return true
+				}).WithTimeout(10 * time.Minute).WithPolling(5 * time.Second).Should(BeTrue())
+			}
+
+			transactionSystemProcessGroups := make(map[fdbv1beta2.ProcessGroupID]fdbv1beta2.None)
+			// Wait until the cluster is upgraded and fully reconciled.
+			Expect(fdbCluster.WaitUntilWithForceReconcile(2, 600, func(cluster *fdbv1beta2.FoundationDBCluster) bool {
+				for _, processGroup := range cluster.Status.ProcessGroups {
+					if processGroup.ProcessClass == fdbv1beta2.ProcessClassStorage {
+						continue
+					}
+					transactionSystemProcessGroups[processGroup.ProcessGroupID] = fdbv1beta2.None{}
+				}
+
+				// Allow soft reconciliation and make sure the running version was updated
+				return cluster.Status.Generations.Reconciled == cluster.Generation && cluster.Status.RunningVersion == targetVersion
+			})).NotTo(HaveOccurred())
+
+			// Get the desired process counts based on the current cluster configuration
+			processCounts, err := fdbCluster.GetProcessCounts()
+			Expect(err).NotTo(HaveOccurred())
+			expectedProcessCounts := (processCounts.Total()-processCounts.Storage)*2 + 5
+			Expect(len(transactionSystemProcessGroups)).To(BeNumerically("<=", expectedProcessCounts))
+		},
+		EntryDescription("Upgrade from %[1]s to %[2]s"),
+		fixtures.GenerateUpgradeTableEntries(testOptions),
+	)
 })
diff --git a/internal/configmap_helper_test.go b/internal/configmap_helper_test.go
index c88c3ed3..06347ae2 100644
--- a/internal/configmap_helper_test.go
+++ b/internal/configmap_helper_test.go
@@ -182,6 +182,54 @@ var _ = Describe("configmap_helper", func() {
 			})
 		})
 
+		Context("with multiple log servers per disk", func() {
+			BeforeEach(func() {
+				cluster.Status.LogServersPerDisk = []int{1, 2}
+			})
+
+			When("using the split image", func() {
+				BeforeEach(func() {
+					cluster.Status.ImageTypes = []fdbv1beta2.ImageType{"split"}
+				})
+				It("includes the data for both configurations", func() {
+					expectedConf, err := GetMonitorConf(cluster, fdbv1beta2.ProcessClassLog, nil, 1)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(configMap.Data["fdbmonitor-conf-log"]).To(Equal(expectedConf))
+
+					expectedConf, err = GetMonitorConf(cluster, fdbv1beta2.ProcessClassLog, nil, 2)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(configMap.Data["fdbmonitor-conf-log-density-2"]).To(Equal(expectedConf))
+				})
+			})
+
+			When("using the unified image", func() {
+				BeforeEach(func() {
+					cluster.Status.ImageTypes = []fdbv1beta2.ImageType{"unified"}
+				})
+
+				It("includes the data for both configurations", func() {
+					jsonData, present := configMap.Data["fdbmonitor-conf-log-json"]
+					Expect(present).To(BeTrue())
+					config := monitorapi.ProcessConfiguration{}
+					err = json.Unmarshal([]byte(jsonData), &config)
+					Expect(err).NotTo(HaveOccurred())
+					expectedConfig, err := GetMonitorProcessConfiguration(cluster, fdbv1beta2.ProcessClassLog, 1, FDBImageTypeUnified, nil)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(config).To(Equal(expectedConfig))
+
+					jsonData, present = configMap.Data["fdbmonitor-conf-log-json-multiple"]
+					Expect(present).To(BeTrue())
+					config = monitorapi.ProcessConfiguration{}
+					err = json.Unmarshal([]byte(jsonData), &config)
+					Expect(err).NotTo(HaveOccurred())
+					expectedConfig, err = GetMonitorProcessConfiguration(cluster, fdbv1beta2.ProcessClassLog, 2, FDBImageTypeUnified, nil)
+					Expect(err).NotTo(HaveOccurred())
+					Expect(config).To(Equal(expectedConfig))
+				})
+
+			})
+		})
+
 		Context("with custom resource labels", func() {
 			BeforeEach(func() {
 				cluster.Spec.LabelConfig = fdbv1beta2.LabelConfig{
diff --git a/internal/pod_models_test.go b/internal/pod_models_test.go
index fd321d09..1c4c4517 100644
--- a/internal/pod_models_test.go
+++ b/internal/pod_models_test.go
@@ -660,7 +660,7 @@ var _ = Describe("pod_models", func() {
 				})
 			})
 
-			When("running running a log with multiple storage servers per disk", func() {
+			When("creating a log with multiple storage servers per disk", func() {
 				BeforeEach(func() {
 					cluster.Spec.StorageServersPerPod = 2
 					spec, err = GetPodSpec(cluster, fdbv1beta2.ProcessClassLog, 1)
@@ -714,6 +714,62 @@ var _ = Describe("pod_models", func() {
 				})
 			})
 
+			When("running multiple log servers per disk", func() {
+				BeforeEach(func() {
+					cluster.Spec.LogServersPerPod = 2
+					spec, err = GetPodSpec(cluster, fdbv1beta2.ProcessClassLog, 1)
+					Expect(err).NotTo(HaveOccurred())
+				})
+
+				It("should pass the process count to the main container", func() {
+					mainContainer := spec.Containers[0]
+					Expect(mainContainer.Name).To(Equal(fdbv1beta2.MainContainerName))
+					Expect(mainContainer.Args).To(Equal([]string{
+						"--input-dir", "/var/dynamic-conf",
+						"--log-path", "/var/log/fdb-trace-logs/monitor.log",
+						"--process-count", "2",
+					}))
+
+					Expect(mainContainer.Env).To(Equal([]corev1.EnvVar{
+						{Name: "FDB_CLUSTER_FILE", Value: "/var/dynamic-conf/fdb.cluster"},
+						{Name: "LOG_SERVERS_PER_POD", Value: "2"},
+						{Name: "FDB_PUBLIC_IP", ValueFrom: &corev1.EnvVarSource{
+							FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.podIP"},
+						}},
+						{Name: "FDB_POD_IP", ValueFrom: &corev1.EnvVarSource{
+							FieldRef: &corev1.ObjectFieldSelector{FieldPath: "status.podIP"},
+						}},
+						{Name: "FDB_MACHINE_ID", ValueFrom: &corev1.EnvVarSource{
+							FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"},
+						}},
+						{Name: "FDB_ZONE_ID", ValueFrom: &corev1.EnvVarSource{
+							FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"},
+						}},
+						{Name: "FDB_INSTANCE_ID", Value: "log-1"},
+						{Name: "FDB_IMAGE_TYPE", Value: "unified"},
+						{Name: "FDB_POD_NAME", ValueFrom: &corev1.EnvVarSource{
+							FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"},
+						}},
+						{Name: "FDB_POD_NAMESPACE", ValueFrom: &corev1.EnvVarSource{
+							FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.namespace"},
+						}},
+					}))
+				})
+
+				It("mounts the multiple-storage config map", func() {
+					Expect(spec.Volumes[2]).To(Equal(corev1.Volume{
+						Name: "config-map",
+						VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{
+							LocalObjectReference: corev1.LocalObjectReference{Name: fmt.Sprintf("%s-config", cluster.Name)},
+							Items: []corev1.KeyToPath{
+								{Key: "fdbmonitor-conf-log-json-multiple", Path: "config.json"},
+								{Key: ClusterFileKey, Path: "fdb.cluster"},
+							},
+						}},
+					}))
+				})
+			})
+
 			Context("with an instance that is crash looping", func() {
 				BeforeEach(func() {
 					cluster.Spec.Buggify.CrashLoop = []fdbv1beta2.ProcessGroupID{"storage-1"}
@@ -3332,7 +3388,7 @@ var _ = Describe("pod_models", func() {
 					},
 				}
 
-				storageServersPerPod, err := GetStorageServersPerPodForPod(pod)
+				storageServersPerPod, err := GetServersPerPodForPod(pod, fdbv1beta2.ProcessClassStorage)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(storageServersPerPod).To(Equal(1))
 			})
@@ -3353,7 +3409,7 @@ var _ = Describe("pod_models", func() {
 					},
 				}
 
-				storageServersPerPod, err := GetStorageServersPerPodForPod(pod)
+				storageServersPerPod, err := GetServersPerPodForPod(pod, fdbv1beta2.ProcessClassStorage)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(storageServersPerPod).To(Equal(2))
 			})
@@ -3369,7 +3425,7 @@ var _ = Describe("pod_models", func() {
 					},
 				}
 
-				storageServersPerPod, err := GetStorageServersPerPodForPod(pod)
+				storageServersPerPod, err := GetServersPerPodForPod(pod, fdbv1beta2.ProcessClassStorage)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(storageServersPerPod).To(Equal(1))
 			})
@@ -3377,7 +3433,7 @@ var _ = Describe("pod_models", func() {
 
 		Context("when pod is nil", func() {
 			It("should return 1", func() {
-				storageServersPerPod, err := GetStorageServersPerPodForPod(nil)
+				storageServersPerPod, err := GetServersPerPodForPod(nil, fdbv1beta2.ProcessClassStorage)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(storageServersPerPod).To(Equal(1))
 			})
@@ -3387,7 +3443,7 @@ var _ = Describe("pod_models", func() {
 			It("should return 1", func() {
 				pod := &corev1.Pod{}
 
-				storageServersPerPod, err := GetStorageServersPerPodForPod(pod)
+				storageServersPerPod, err := GetServersPerPodForPod(pod, fdbv1beta2.ProcessClassStorage)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(storageServersPerPod).To(Equal(1))
 			})
@@ -3399,7 +3455,7 @@ var _ = Describe("pod_models", func() {
 					Spec: corev1.PodSpec{},
 				}
 
-				storageServersPerPod, err := GetStorageServersPerPodForPod(pod)
+				storageServersPerPod, err := GetServersPerPodForPod(pod, fdbv1beta2.ProcessClassStorage)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(storageServersPerPod).To(Equal(1))
 			})