Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add Metrics to Epoch Page #1291

Merged
merged 13 commits into from
Mar 20, 2024
10 changes: 10 additions & 0 deletions api/src/models/api/nova/stats/epoch/IEpochAnalyticStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface IEpochAnalyticStats {
epochIndex?: number;
blockCount?: number;
perPayloadType?: {
transaction?: number;
candidacy?: number;
taggedData?: number;
noPayload?: number;
};
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/stats/epoch/IEpochAnalyticStatsRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IEpochAnalyticStatsRequest {
/**
* The network to search on.
*/
network: string;

/**
* The epoch index to get the stats for.
*/
epochIndex: string;
}
16 changes: 16 additions & 0 deletions api/src/models/influx/nova/IInfluxDbCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ export interface IInfluxAnalyticsCache {
delegatorsCount?: string;
}

interface IEpochAnalyticStats {
epochIndex: number;
blockCount: number;
perPayloadType: {
transaction: number;
taggedData: number;
candidacy: number;
noPayload: number;
};
}

/**
* The epoch stats cache. Map epoch index to stats.
*/
export type IInfluxEpochAnalyticsCache = Map<number, IEpochAnalyticStats>;

/**
* The helper to initialize empty maps
* @returns The initial cache object
Expand Down
6 changes: 6 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,10 @@ export const routes: IRoute[] = [
func: "get",
sign: true,
},
{
path: "/nova/epoch/stats/:network/:epochIndex",
method: "get",
folder: "nova/epoch/influx",
func: "get",
},
];
58 changes: 58 additions & 0 deletions api/src/routes/nova/epoch/influx/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ServiceFactory } from "../../../../factories/serviceFactory";
import { IResponse } from "../../../../models/api/nova/IResponse";
import { IEpochAnalyticStats } from "../../../../models/api/nova/stats/epoch/IEpochAnalyticStats";
import { IEpochAnalyticStatsRequest } from "../../../../models/api/nova/stats/epoch/IEpochAnalyticStatsRequest";
import { IConfiguration } from "../../../../models/configuration/IConfiguration";
import { NOVA } from "../../../../models/db/protocolVersion";
import { NetworkService } from "../../../../services/networkService";
import { InfluxServiceNova } from "../../../../services/nova/influx/influxServiceNova";
import { NodeInfoService } from "../../../../services/nova/nodeInfoService";
import { ValidationHelper } from "../../../../utils/validationHelper";

/**
* The response with the current cached data.
*/
type IEpochAnalyticStatsReponse = IEpochAnalyticStats & IResponse;

/**
* Find the object from the network.
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(_: IConfiguration, request: IEpochAnalyticStatsRequest): Promise<IEpochAnalyticStatsReponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.number(Number(request.epochIndex), "epochIndex");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const influxService = ServiceFactory.get<InfluxServiceNova>(`influxdb-${request.network}`);
const nodeService = ServiceFactory.get<NodeInfoService>(`node-info-${request.network}`);
const protocolParameters = await nodeService.getProtocolParameters();

if (!influxService || !protocolParameters) {
return { error: "Influx service not found for this network." };
}

const epochIndex = Number.parseInt(request.epochIndex, 10);
let maybeEpochStats = influxService.getEpochAnalyticStats(epochIndex);
if (!maybeEpochStats) {
maybeEpochStats = await influxService.fetchAnalyticsForEpoch(epochIndex, protocolParameters);
}

return maybeEpochStats
? {
epochIndex: maybeEpochStats.epochIndex,
blockCount: maybeEpochStats.blockCount,
perPayloadType: maybeEpochStats.perPayloadType,
}
: {
message: `Could not fetch epoch analytics for ${request.epochIndex}`,
};
}
2 changes: 1 addition & 1 deletion api/src/services/influx/influxClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DayKey, DAY_KEY_FORMAT, ITimedEntry } from "../../models/influx/types";
/**
* N of nanoseconds in a millsecond.
*/
const NANOSECONDS_IN_MILLISECOND = 1000000;
export const NANOSECONDS_IN_MILLISECOND = 1000000;

/**
* The InfluxDb Client wrapper.
Expand Down
10 changes: 10 additions & 0 deletions api/src/services/nova/influx/influxQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ export const OUTPUTS_DAILY_QUERY = {
`,
};

export const EPOCH_STATS_QUERY_BY_EPOCH_INDEX = `
SELECT
sum("transaction_count") AS "transaction",
sum("tagged_data_count") AS "taggedData",
sum("candidacy_announcement_count") AS "candidacy",
sum("no_payload_count") AS "noPayload"
FROM "iota_block_activity"
WHERE time >= $from and time <= $to
`;

export const TOKENS_HELD_BY_OUTPUTS_DAILY_QUERY = {
full: `
SELECT
Expand Down
145 changes: 140 additions & 5 deletions api/src/services/nova/influx/influxServiceNova.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { InfluxDB } from "influx";
/* eslint-disable import/no-unresolved */
import { ProtocolParameters } from "@iota/sdk-nova";
import { InfluxDB, toNanoDate } from "influx";
import moment from "moment";
import cron from "node-cron";
import {
BLOCK_DAILY_QUERY,
OUTPUTS_DAILY_QUERY,
ACCOUNT_ACTIVITY_DAILY_QUERY,
ADDRESSES_WITH_BALANCE_DAILY_QUERY,
ACCOUNT_ADDRESSES_WITH_BALANCE_TOTAL_QUERY,
ANCHOR_ACTIVITY_DAILY_QUERY,
BLOCK_DAILY_QUERY,
BLOCK_ISSUERS_DAILY_QUERY,
DELEGATIONS_ACTIVITY_DAILY_QUERY,
DELEGATION_ACTIVITY_DAILY_QUERY,
Expand All @@ -16,7 +20,6 @@ import {
NATIVE_TOKENS_STAT_TOTAL_QUERY,
NFT_ACTIVITY_DAILY_QUERY,
NFT_STAT_TOTAL_QUERY,
OUTPUTS_DAILY_QUERY,
STAKING_ACTIVITY_DAILY_QUERY,
STORAGE_DEPOSIT_DAILY_QUERY,
STORAGE_DEPOSIT_TOTAL_QUERY,
Expand All @@ -27,11 +30,19 @@ import {
TRANSACTION_DAILY_QUERY,
UNLOCK_CONDITIONS_PER_TYPE_DAILY_QUERY,
VALIDATORS_ACTIVITY_DAILY_QUERY,
EPOCH_STATS_QUERY_BY_EPOCH_INDEX,
DELEGATORS_TOTAL_QUERY,
} from "./influxQueries";
import { ServiceFactory } from "../../../factories/serviceFactory";
import logger from "../../../logger";
import { IEpochAnalyticStats } from "../../../models/api/nova/stats/epoch/IEpochAnalyticStats";
import { INetwork } from "../../../models/db/INetwork";
import { IInfluxAnalyticsCache, IInfluxDailyCache, initializeEmptyDailyCache } from "../../../models/influx/nova/IInfluxDbCache";
import {
IInfluxAnalyticsCache,
IInfluxDailyCache,
IInfluxEpochAnalyticsCache,
initializeEmptyDailyCache,
} from "../../../models/influx/nova/IInfluxDbCache";
import {
IAccountActivityDailyInflux,
IActiveAddressesDailyInflux,
Expand All @@ -57,8 +68,22 @@ import {
IValidatorsActivityDailyInflux,
} from "../../../models/influx/nova/IInfluxTimedEntries";
import { ITimedEntry } from "../../../models/influx/types";
import { InfluxDbClient } from "../../influx/influxClient";
import { epochIndexToUnixTimeRangeConverter, unixTimestampToEpochIndexConverter } from "../../../utils/nova/novaTimeUtils";
import { InfluxDbClient, NANOSECONDS_IN_MILLISECOND } from "../../influx/influxClient";
import { NodeInfoService } from "../nodeInfoService";

type EpochUpdate = ITimedEntry & {
epochIndex: number;
taggedData: number;
candidacy: number;
transaction: number;
noPayload: number;
};

/**
* Epoch analyitics cache MAX size.
*/
const EPOCH_CACHE_MAX = 20;
/**
* The collect graph data interval cron expression.
* Every hour at 59 min 55 sec
Expand All @@ -71,6 +96,12 @@ const COLLECT_GRAPHS_DATA_CRON = "55 59 * * * *";
*/
const COLLECT_ANALYTICS_DATA_CRON = "55 58 * * * *";

/*
* The collect analytics data interval cron expression.
* Every 10 minutes
*/
const COLLECT_EPOCH_ANALYTICS_DATA_CRON = "*/10 * * * *";

export class InfluxServiceNova extends InfluxDbClient {
/**
* The InfluxDb Client.
Expand All @@ -82,6 +113,11 @@ export class InfluxServiceNova extends InfluxDbClient {
*/
protected readonly _dailyCache: IInfluxDailyCache;

/**
* The current influx epoch analytics cache instance.
*/
protected readonly _epochCache: IInfluxEpochAnalyticsCache;

/**
* The current influx analytics cache instance.
*/
Expand All @@ -95,6 +131,7 @@ export class InfluxServiceNova extends InfluxDbClient {
constructor(network: INetwork) {
super(network);
this._dailyCache = initializeEmptyDailyCache();
this._epochCache = new Map();
this._analyticsCache = {};
}

Expand Down Expand Up @@ -206,6 +243,15 @@ export class InfluxServiceNova extends InfluxDbClient {
return this._analyticsCache.delegatorsCount;
}

public getEpochAnalyticStats(epochIndex: number): IEpochAnalyticStats | undefined {
return this._epochCache.get(epochIndex);
}

public async fetchAnalyticsForEpoch(epochIndex: number, parameters: ProtocolParameters) {
await this.collectEpochStatsByIndex(epochIndex, parameters);
return this._epochCache.get(epochIndex);
}

protected setupDataCollection() {
const network = this._network.network;
logger.verbose(`[InfluxNova] Setting up data collection for (${network}).`);
Expand All @@ -214,6 +260,8 @@ export class InfluxServiceNova extends InfluxDbClient {
void this.collectGraphsDaily();
// eslint-disable-next-line no-void
void this.collectAnalytics();
// eslint-disable-next-line no-void
void this.collectEpochStats();

if (this._client) {
cron.schedule(COLLECT_GRAPHS_DATA_CRON, async () => {
Expand All @@ -225,6 +273,11 @@ export class InfluxServiceNova extends InfluxDbClient {
// eslint-disable-next-line no-void
void this.collectAnalytics();
});

cron.schedule(COLLECT_EPOCH_ANALYTICS_DATA_CRON, async () => {
// eslint-disable-next-line no-void
void this.collectEpochStats();
});
}
}

Expand Down Expand Up @@ -372,4 +425,86 @@ export class InfluxServiceNova extends InfluxDbClient {
logger.warn(`[InfluxNova] Failed refreshing analytics for "${this._network.network}"! Cause: ${err}`);
}
}

/**
* Get the epoch analytics by index and set it in the cache.
* @param epochIndex - The epoch index.
* @param parameters - The protocol parameters information.
*/
private async collectEpochStatsByIndex(epochIndex: number, parameters: ProtocolParameters) {
try {
const epochIndexToUnixTimeRange = epochIndexToUnixTimeRangeConverter(parameters);
const { from, to } = epochIndexToUnixTimeRange(epochIndex);
const fromNano = toNanoDate((moment(Number(from) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());
const toNano = toNanoDate((moment(Number(to) * 1000).valueOf() * NANOSECONDS_IN_MILLISECOND).toString());

await this.queryInflux<EpochUpdate>(EPOCH_STATS_QUERY_BY_EPOCH_INDEX, fromNano, toNano)
.then((results) => {
for (const update of results) {
update.epochIndex = epochIndex;
this.updateEpochCache(update);
}
})
.catch((e) => {
logger.warn(
`[InfluxClient] Query ${EPOCH_STATS_QUERY_BY_EPOCH_INDEX} failed for (${this._network.network}). Cause ${e}`,
);
});
} catch (err) {
logger.warn(`[InfluxNova] Failed refreshing epoch stats for "${this._network.network}". Cause: ${err}`);
}
}

/**
* Get the epoch analytics and set it in the cache.
*/
private async collectEpochStats() {
try {
logger.debug(`[InfluxNova] Collecting epoch stats for "${this._network.network}"`);
const nodeService = ServiceFactory.get<NodeInfoService>(`node-info-${this._network.network}`);
const parameters = await nodeService.getProtocolParameters();
const unixTimestampToEpochIndex = unixTimestampToEpochIndexConverter(parameters);
const epochIndex = unixTimestampToEpochIndex(moment().unix());
// eslint-disable-next-line no-void
void this.collectEpochStatsByIndex(epochIndex, parameters);
} catch (err) {
logger.warn(`[InfluxNova] Failed refreshing epoch stats for "${this._network.network}". Cause: ${err}`);
}
}

private updateEpochCache(update: EpochUpdate) {
if (update.epochIndex !== undefined && !this._epochCache.has(update.epochIndex)) {
const { epochIndex, transaction, candidacy, taggedData, noPayload } = update;
const blockCount = transaction + candidacy + taggedData + noPayload;
this._epochCache.set(epochIndex, {
epochIndex,
blockCount,
perPayloadType: {
transaction,
candidacy,
taggedData,
noPayload,
},
});

logger.debug(`[InfluxNova] Added epoch index "${epochIndex}" to cache for "${this._network.network}"`);

if (this._epochCache.size > EPOCH_CACHE_MAX) {
let lowestIndex: number;
for (const index of this._epochCache.keys()) {
if (!lowestIndex) {
lowestIndex = index;
}

if (epochIndex < lowestIndex) {
lowestIndex = index;
}
}

logger.debug(`[InfluxNova] Deleting epoch index "${lowestIndex}" ("${this._network.network}")`);

this._epochCache.delete(lowestIndex);
}
}
}
}
Loading
Loading