Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a2d6b54
✨ feat: add operation mode failure tracking metric
kyzooghost Nov 26, 2025
9e83c11
♻️ refactor: merge operation mode execution and failure metrics with …
kyzooghost Nov 26, 2025
39abf4b
🔥 fix: remove OperationModeTriggerTotal metric
kyzooghost Nov 26, 2025
db0c049
tmp change for push docker
kyzooghost Nov 26, 2025
56a1546
♻️ refactor: replace Dashboard peekUnpaidLidoProtocolFees with VaultH…
kyzooghost Nov 26, 2025
90af9b2
♻️ refactor: rename LastPeekUnpaidLidoProtocolFees to LastSettleableL…
kyzooghost Nov 26, 2025
8fe2fa9
🔊 fix: change peekYieldReport error logging from DEBUG to ERROR
kyzooghost Nov 26, 2025
66c92a6
🧑‍💻 chore: add manual integration test script for YieldManagerContrac…
kyzooghost Nov 26, 2025
8ae2f74
✨ feat: implement getSignerAddress in ViemBlockchainClientAdapter
kyzooghost Nov 26, 2025
945c0b9
🐛 fix: pass signer address to peekYieldReport simulation
kyzooghost Nov 26, 2025
0e7f59a
♻️ refactor: accept all RebalanceDirection values in recordRebalance
kyzooghost Nov 26, 2025
eb8590c
✅ test: record rebalance metrics for NONE direction case
kyzooghost Nov 26, 2025
95891ca
♻️ refactor: always record metrics and add logging for undefined rece…
kyzooghost Nov 26, 2025
b1456c2
🔊 feat: add warning logs for missing yield report and withdrawal event
kyzooghost Nov 26, 2025
f0cc598
✨ feat: implement getTxReceipt method in ViemBlockchainClientAdapter
kyzooghost Nov 26, 2025
05fd66d
update YieldManager abi
kyzooghost Nov 26, 2025
a25acc3
🔧 chore: add test files to eslintignore and remove trailing newline
kyzooghost Nov 26, 2025
839555e
♻️ refactor: remove early return validation from recordRebalance
kyzooghost Nov 26, 2025
1f9ccf8
🐛 fix: record NONE direction for no-rebalance metrics
kyzooghost Nov 26, 2025
972a5b2
🔊 feat: add logging when withdrawal balance below threshold
kyzooghost Nov 26, 2025
529c971
🔊 feat: add logging when withdrawal balance below threshold
kyzooghost Nov 26, 2025
c006be2
🔊 feat: include decision result in yield reporting log
kyzooghost Nov 26, 2025
d759983
Revert "🐛 fix: record NONE direction for no-rebalance metrics"
kyzooghost Nov 26, 2025
c8b4276
Revert "✅ test: record rebalance metrics for NONE direction case"
kyzooghost Nov 26, 2025
d80170f
🔊 feat: add logging before all early returns in class files
kyzooghost Nov 27, 2025
6e417e1
🔊 feat: split validator array logging into info and debug levels
kyzooghost Nov 27, 2025
51bae28
🔊 feat: change debug logs to info level in BeaconChainStakingClient
kyzooghost Nov 27, 2025
747699b
♻️ refactor: split debug logs into info summary and debug details in …
kyzooghost Nov 27, 2025
b7f9f42
♻️ refactor: apply logging pattern to YieldManagerContractClient
kyzooghost Nov 27, 2025
35ba5d5
✅ test: add test case for undefined nodes in ConsensysStakingApiClient
kyzooghost Nov 27, 2025
88c9eb1
✨ feat: add gauge metric for total pending partial withdrawals
kyzooghost Nov 27, 2025
55d260b
♻️ refactor: make gauge setter methods synchronous
kyzooghost Nov 27, 2025
0a8dcb0
✨ feat: add refreshGaugeMetrics to OperationModeSelector
kyzooghost Nov 27, 2025
ca350e0
🔊 feat: add info log for pending withdrawal count in BeaconNodeApiClient
kyzooghost Nov 27, 2025
c02d6f3
🔊 feat: improve Web3SignerClientAdapter initialization log message
kyzooghost Nov 27, 2025
ee440fc
✨ feat: add LOG_LEVEL environment variable support
kyzooghost Nov 27, 2025
ec3b19c
🔊 feat: add detailed error logging for refreshGaugeMetrics failures
kyzooghost Nov 27, 2025
8636544
🐛 fix: convert GraphQL string responses to bigint in ConsensysStaking…
kyzooghost Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
path: linea-native-yield-automation-service-docker-image.tar.gz
- name: Build and push native-yield-automation-service image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
if: ${{ env.PUSH_IMAGE == 'true' || github.event_name == 'workflow_dispatch' }}
# if: ${{ env.PUSH_IMAGE == 'true' || github.event_name == 'workflow_dispatch' }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Commented-out condition causes unintended Docker image pushes

The if condition controlling when to push Docker images has been commented out. This causes the "Build and push native-yield-automation-service image" step to always run with push: true, even when PUSH_IMAGE is set to false. The workflow was designed to only push images when PUSH_IMAGE == 'true' or during manual workflow_dispatch events. This change is not mentioned in the PR description and appears to be accidentally committed debugging code that would result in unwanted image pushes to the registry.

Fix in Cursor Fix in Web

with:
context: ./
file: ./native-yield-operations/automation-service/Dockerfile
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IMetricsService } from "@consensys/linea-shared-utils";
import {
LineaNativeYieldAutomationServiceMetrics,
OperationTrigger,
OperationModeExecutionStatus,
} from "../../core/metrics/LineaNativeYieldAutomationServiceMetrics.js";
import { RebalanceDirection } from "../../core/entities/RebalanceRequirement.js";
import { Address, Hex } from "viem";
Expand Down Expand Up @@ -96,16 +96,10 @@ export class NativeYieldAutomationMetricsUpdater implements INativeYieldAutomati
["vault_address"],
);

this.metricsService.createCounter(
LineaNativeYieldAutomationServiceMetrics.OperationModeTriggerTotal,
"Operation mode triggers grouped by mode and triggers",
["mode", "trigger"],
);

this.metricsService.createCounter(
LineaNativeYieldAutomationServiceMetrics.OperationModeExecutionTotal,
"Operation mode executions grouped by mode",
["mode"],
"Operation mode executions grouped by mode and status",
["mode", "status"],
);

this.metricsService.createHistogram(
Expand Down Expand Up @@ -291,26 +285,18 @@ export class NativeYieldAutomationMetricsUpdater implements INativeYieldAutomati
}

/**
* Increments the counter for operation mode triggers, grouped by mode and trigger type.
*
* @param {OperationMode} mode - The operation mode that was triggered.
* @param {OperationTrigger} trigger - The trigger that caused the mode to be activated.
*/
public incrementOperationModeTrigger(mode: OperationMode, trigger: OperationTrigger): void {
this.metricsService.incrementCounter(LineaNativeYieldAutomationServiceMetrics.OperationModeTriggerTotal, {
mode,
trigger,
});
}

/**
* Increments the counter for operation mode executions, grouped by mode.
* Increments the counter for operation mode executions, grouped by mode and status.
*
* @param {OperationMode} mode - The operation mode that was executed.
* @param {OperationModeExecutionStatus} [status=OperationModeExecutionStatus.Success] - The execution status. Defaults to Success.
*/
public incrementOperationModeExecution(mode: OperationMode): void {
public incrementOperationModeExecution(
mode: OperationMode,
status: OperationModeExecutionStatus = OperationModeExecutionStatus.Success,
): void {
this.metricsService.incrementCounter(LineaNativeYieldAutomationServiceMetrics.OperationModeExecutionTotal, {
mode,
status,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IMetricsService } from "@consensys/linea-shared-utils";
import { NativeYieldAutomationMetricsUpdater } from "../NativeYieldAutomationMetricsUpdater.js";
import {
LineaNativeYieldAutomationServiceMetrics,
OperationTrigger,
OperationModeExecutionStatus,
} from "../../../core/metrics/LineaNativeYieldAutomationServiceMetrics.js";
import { RebalanceDirection } from "../../../core/entities/RebalanceRequirement.js";
import { OperationMode } from "../../../core/enums/OperationModeEnums.js";
Expand Down Expand Up @@ -95,15 +95,10 @@ describe("NativeYieldAutomationMetricsUpdater", () => {
"Lido fees paid by automation per vault",
["vault_address"],
);
expect(metricsService.createCounter).toHaveBeenCalledWith(
LineaNativeYieldAutomationServiceMetrics.OperationModeTriggerTotal,
"Operation mode triggers grouped by mode and triggers",
["mode", "trigger"],
);
expect(metricsService.createCounter).toHaveBeenCalledWith(
LineaNativeYieldAutomationServiceMetrics.OperationModeExecutionTotal,
"Operation mode executions grouped by mode",
["mode"],
"Operation mode executions grouped by mode and status",
["mode", "status"],
);
expect(metricsService.createHistogram).toHaveBeenCalledWith(
LineaNativeYieldAutomationServiceMetrics.OperationModeExecutionDurationSeconds,
Expand Down Expand Up @@ -358,29 +353,42 @@ describe("NativeYieldAutomationMetricsUpdater", () => {
});
});

it("increments operation mode trigger counter", () => {
it("increments operation mode execution counter with default success status", () => {
const metricsService = createMetricsServiceMock();
const updater = new NativeYieldAutomationMetricsUpdater(metricsService);
jest.clearAllMocks();

updater.incrementOperationModeTrigger(OperationMode.YIELD_REPORTING_MODE, OperationTrigger.TIMEOUT);
updater.incrementOperationModeExecution(OperationMode.OSSIFICATION_PENDING_MODE);

expect(metricsService.incrementCounter).toHaveBeenCalledWith(
LineaNativeYieldAutomationServiceMetrics.OperationModeTriggerTotal,
{ mode: OperationMode.YIELD_REPORTING_MODE, trigger: OperationTrigger.TIMEOUT },
LineaNativeYieldAutomationServiceMetrics.OperationModeExecutionTotal,
{ mode: OperationMode.OSSIFICATION_PENDING_MODE, status: OperationModeExecutionStatus.Success },
);
});

it("increments operation mode execution counter", () => {
it("increments operation mode execution counter with explicit success status", () => {
const metricsService = createMetricsServiceMock();
const updater = new NativeYieldAutomationMetricsUpdater(metricsService);
jest.clearAllMocks();

updater.incrementOperationModeExecution(OperationMode.OSSIFICATION_PENDING_MODE);
updater.incrementOperationModeExecution(OperationMode.YIELD_REPORTING_MODE, OperationModeExecutionStatus.Success);

expect(metricsService.incrementCounter).toHaveBeenCalledWith(
LineaNativeYieldAutomationServiceMetrics.OperationModeExecutionTotal,
{ mode: OperationMode.YIELD_REPORTING_MODE, status: OperationModeExecutionStatus.Success },
);
});

it("increments operation mode execution counter with failure status", () => {
const metricsService = createMetricsServiceMock();
const updater = new NativeYieldAutomationMetricsUpdater(metricsService);
jest.clearAllMocks();

updater.incrementOperationModeExecution(OperationMode.YIELD_REPORTING_MODE, OperationModeExecutionStatus.Failure);

expect(metricsService.incrementCounter).toHaveBeenCalledWith(
LineaNativeYieldAutomationServiceMetrics.OperationModeExecutionTotal,
{ mode: OperationMode.OSSIFICATION_PENDING_MODE },
{ mode: OperationMode.YIELD_REPORTING_MODE, status: OperationModeExecutionStatus.Failure },
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const createMetricsUpdaterMock = (): jest.Mocked<INativeYieldAutomationMetricsUp
addLidoFeesPaid: jest.fn(),
incrementLidoVaultAccountingReport: jest.fn(),
incrementOperationModeExecution: jest.fn(),
incrementOperationModeTrigger: jest.fn(),
addValidatorPartialUnstakeAmount: jest.fn(),
incrementValidatorExit: jest.fn(),
}) as unknown as jest.Mocked<INativeYieldAutomationMetricsUpdater>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const createMetricsUpdaterMock = () => {
addNodeOperatorFeesPaid: jest.fn(),
addLiabilitiesPaid: jest.fn(),
addLidoFeesPaid: jest.fn(),
incrementOperationModeTrigger: jest.fn(),
incrementOperationModeExecution: jest.fn(),
recordOperationModeDuration: jest.fn(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum OperationMode {
YIELD_REPORTING_MODE = "YIELD_REPORTING_MODE",
OSSIFICATION_PENDING_MODE = "OSSIFICATION_PENDING_MODE",
OSSIFICATION_COMPLETE_MODE = "OSSIFICATION_COMPLETE_MODE",
UNKNOWN = "UNKNOWN",
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Address, Hex } from "viem";
import { RebalanceDirection } from "../entities/RebalanceRequirement.js";
import { OperationMode } from "../enums/OperationModeEnums.js";
import { OperationTrigger } from "./LineaNativeYieldAutomationServiceMetrics.js";
import { OperationModeExecutionStatus } from "./LineaNativeYieldAutomationServiceMetrics.js";

export interface INativeYieldAutomationMetricsUpdater {
recordRebalance(direction: RebalanceDirection.STAKE | RebalanceDirection.UNSTAKE, amountGwei: number): void;
Expand All @@ -28,9 +28,7 @@ export interface INativeYieldAutomationMetricsUpdater {

addLidoFeesPaid(vaultAddress: Address, amountGwei: number): void;

incrementOperationModeTrigger(mode: OperationMode, trigger: OperationTrigger): void;

incrementOperationModeExecution(mode: OperationMode): void;
incrementOperationModeExecution(mode: OperationMode, status?: OperationModeExecutionStatus): void;

recordOperationModeDuration(mode: OperationMode, durationSeconds: number): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,10 @@ export enum LineaNativeYieldAutomationServiceMetrics {
// N.B. Only accounts for payments by the automation service, but external actors can also trigger payment
LidoFeesPaidTotal = "linea_native_yield_automation_service_lido_fees_paid_total",

// Counter that increments each time an operation mode is triggered.
// Counter that increments each time an operation mode completes execution.
// Labels:
// i.) `mode`
// i.) `operation_trigger` - VaultsReportDataUpdated_event vs timeout
OperationModeTriggerTotal = "linea_native_yield_automation_service_operation_mode_trigger_total",

// Counter that increments each time an operation mode completes execution.
// Single label `mode`
// ii.) `status` - OperationModeExecutionStatus.Success | OperationModeExecutionStatus.Failure
OperationModeExecutionTotal = "linea_native_yield_automation_service_operation_mode_execution_total",

// Histogram that tracks time for each operation mode run.
Expand All @@ -73,3 +69,8 @@ export enum OperationTrigger {
VAULTS_REPORT_DATA_UPDATED_EVENT = "VAULTS_REPORT_DATA_UPDATED_EVENT",
TIMEOUT = "TIMEOUT",
}

export enum OperationModeExecutionStatus {
Success = "success",
Failure = "failure",
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IOperationModeSelector } from "../core/services/operation-mode/IOperati
import { IOperationModeProcessor } from "../core/services/operation-mode/IOperationModeProcessor.js";
import { INativeYieldAutomationMetricsUpdater } from "../core/metrics/INativeYieldAutomationMetricsUpdater.js";
import { OperationMode } from "../core/enums/OperationModeEnums.js";
import { OperationModeExecutionStatus } from "../core/metrics/LineaNativeYieldAutomationServiceMetrics.js";

/**
* Selects and executes the appropriate operation mode based on the yield provider's ossification state.
Expand Down Expand Up @@ -35,8 +36,7 @@ export class OperationModeSelector implements IOperationModeSelector {
private readonly ossificationCompleteOperationModeProcessor: IOperationModeProcessor,
private readonly yieldProvider: Address,
private readonly contractReadRetryTimeMs: number,
) {
}
) {}

/**
* Starts the operation mode selection loop.
Expand Down Expand Up @@ -81,30 +81,44 @@ export class OperationModeSelector implements IOperationModeSelector {
*/
private async selectOperationModeLoop(): Promise<void> {
while (this.isRunning) {
let currentMode: OperationMode = OperationMode.UNKNOWN;
try {
const [isOssificationInitiated, isOssified] = await Promise.all([
this.yieldManagerContractClient.isOssificationInitiated(this.yieldProvider),
this.yieldManagerContractClient.isOssified(this.yieldProvider),
]);

if (isOssified) {
currentMode = OperationMode.OSSIFICATION_COMPLETE_MODE;
this.logger.info("Selected OSSIFICATION_COMPLETE_MODE");
await this.ossificationCompleteOperationModeProcessor.process();
this.logger.info("Completed OSSIFICATION_COMPLETE_MODE");
this.metricsUpdater.incrementOperationModeExecution(OperationMode.OSSIFICATION_COMPLETE_MODE);
this.metricsUpdater.incrementOperationModeExecution(
OperationMode.OSSIFICATION_COMPLETE_MODE,
OperationModeExecutionStatus.Success,
);
} else if (isOssificationInitiated) {
currentMode = OperationMode.OSSIFICATION_PENDING_MODE;
this.logger.info("Selected OSSIFICATION_PENDING_MODE");
await this.ossificationPendingOperationModeProcessor.process();
this.logger.info("Completed OSSIFICATION_PENDING_MODE");
this.metricsUpdater.incrementOperationModeExecution(OperationMode.OSSIFICATION_PENDING_MODE);
this.metricsUpdater.incrementOperationModeExecution(
OperationMode.OSSIFICATION_PENDING_MODE,
OperationModeExecutionStatus.Success,
);
} else {
currentMode = OperationMode.YIELD_REPORTING_MODE;
this.logger.info("Selected YIELD_REPORTING_MODE");
await this.yieldReportingOperationModeProcessor.process();
this.logger.info("Completed YIELD_REPORTING_MODE");
this.metricsUpdater.incrementOperationModeExecution(OperationMode.YIELD_REPORTING_MODE);
this.metricsUpdater.incrementOperationModeExecution(
OperationMode.YIELD_REPORTING_MODE,
OperationModeExecutionStatus.Success,
);
}
} catch (error) {
this.logger.error(`selectOperationModeLoop error, retrying in ${this.contractReadRetryTimeMs}ms`, { error });
this.metricsUpdater.incrementOperationModeExecution(currentMode, OperationModeExecutionStatus.Failure);
await wait(this.contractReadRetryTimeMs);
}
}
Expand Down
Loading
Loading