Skip to content

Commit

Permalink
feat: augment gateway metrics query type
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Mar 31, 2022
1 parent 041d431 commit 2f2d3c8
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 56 deletions.
103 changes: 67 additions & 36 deletions packages/gateway/src/durable-objects/summary-metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import {
} from '../utils/histogram.js'

/**
* @typedef {'CID'|'CID+PATH'} QueryType
*
* @typedef {Object} SummaryMetrics
* @property {number} totalWinnerResponseTime total response time of the requests
* @property {number} totalWinnerSuccessfulRequests total number of successful requests
* @property {number} totalCachedResponseTime total response time to forward cached responses
* @property {number} totalCachedResponses total number of cached responses
* @property {BigInt} totalContentLengthBytes total content length of responses
* @property {BigInt} totalCachedContentLengthBytes total content length of cached responses
* @property {Record<QueryType, number>} totalResponsesByQueryType
* @property {Record<string, number>} contentLengthHistogram
* @property {Record<string, number>} responseTimeHistogram
*
* @typedef {Object} FetchStats
* @property {number} responseTime number of milliseconds to get response
* @property {number} contentLength content length header content
* @property {string} [pathname] fetched pathname
* @property {number} [responseTime] number of milliseconds to get response
* @property {number} [contentLength] content length header content
*/

// Key to track total time for winner gateway to respond
Expand All @@ -35,6 +39,8 @@ const TOTAL_CACHED_CONTENT_LENGTH_BYTES_ID = 'totalCachedContentLengthBytes'
const CONTENT_LENGTH_HISTOGRAM_ID = 'contentLengthHistogram'
// Key to track response time histogram
const RESPONSE_TIME_HISTOGRAM_ID = 'responseTimeHistogram'
// Key to track responses by query type
const TOTAL_RESPONSES_BY_QUERY_TYPE_ID = 'totalResponsesByQueryType'

/**
* Durable Object for keeping summary metrics of nft.storage Gateway
Expand All @@ -57,19 +63,23 @@ export class SummaryMetrics0 {
// Total cached requests
this.totalCachedResponses =
(await this.state.storage.get(TOTAL_CACHED_RESPONSES_ID)) || 0
// Total content length responses
/** @type {BigInt} */
this.totalContentLengthBytes =
(await this.state.storage.get(TOTAL_CONTENT_LENGTH_BYTES_ID)) ||
BigInt(0)
// Total cached content length responses
/** @type {BigInt} */
this.totalCachedContentLengthBytes =
(await this.state.storage.get(TOTAL_CACHED_CONTENT_LENGTH_BYTES_ID)) ||
BigInt(0)
// Content length histogram
/** @type {Record<QueryType, number>} */
this.totalResponsesByQueryType =
(await this.state.storage.get(TOTAL_RESPONSES_BY_QUERY_TYPE_ID)) ||
createResponsesByQueryTypeObject()
/** @type {Record<string, number>} */
this.contentLengthHistogram =
(await this.state.storage.get(CONTENT_LENGTH_HISTOGRAM_ID)) ||
createContentLengthHistogramObject()
// Response time histogram
/** @type {Record<string, number>} */
this.responseTimeHistogram =
(await this.state.storage.get(RESPONSE_TIME_HISTOGRAM_ID)) ||
createResponseTimeHistogramObject()
Expand All @@ -94,6 +104,7 @@ export class SummaryMetrics0 {
totalContentLengthBytes: this.totalContentLengthBytes.toString(),
totalCachedContentLengthBytes:
this.totalCachedContentLengthBytes.toString(),
totalResponsesByQueryType: this.totalResponsesByQueryType,
contentLengthHistogram: this.contentLengthHistogram,
responseTimeHistogram: this.responseTimeHistogram,
})
Expand All @@ -109,13 +120,15 @@ export class SummaryMetrics0 {
switch (url.pathname) {
case '/metrics/winner':
await this._updateWinnerMetrics(data)
return new Response()
break
case '/metrics/cache':
await this._updatedCacheMetrics(data)
return new Response()
break
default:
return new Response('Not found', { status: 404 })
throw new Error('Not found')
}

return new Response()
}

/**
Expand All @@ -126,7 +139,7 @@ export class SummaryMetrics0 {
this.totalCachedResponseTime += stats.responseTime
this.totalCachedResponses += 1
this.totalCachedContentLengthBytes += BigInt(stats.contentLength)
this._updateContentLengthMetrics(stats)
this._updateContentMetrics(stats)
this._updateResponseTimeHistogram(stats)
// Save updated metrics
await Promise.all([
Expand Down Expand Up @@ -164,7 +177,7 @@ export class SummaryMetrics0 {
// Updated Metrics
this.totalWinnerResponseTime += stats.responseTime
this.totalWinnerSuccessfulRequests += 1
this._updateContentLengthMetrics(stats)
this._updateContentMetrics(stats)
this._updateResponseTimeHistogram(stats)
// Save updated Metrics
await Promise.all([
Expand All @@ -188,57 +201,75 @@ export class SummaryMetrics0 {
RESPONSE_TIME_HISTOGRAM_ID,
this.responseTimeHistogram
),
this.state.storage.put(
TOTAL_RESPONSES_BY_QUERY_TYPE_ID,
this.totalResponsesByQueryType
),
])
}

/**
* @param {FetchStats} stats
*/
_updateContentLengthMetrics(stats) {
_updateContentMetrics(stats) {
// Content Length
this.totalContentLengthBytes += BigInt(stats.contentLength)

// Update histogram
const tmpHistogram = {
...this.contentLengthHistogram,
}

// Get all the histogram buckets where the content size is smaller
const histogramCandidates = contentLengthHistogram.filter(
(h) => stats.contentLength < h
this.contentLengthHistogram = getUpdatedHistogram(
this.contentLengthHistogram,
contentLengthHistogram,
stats.contentLength
)
histogramCandidates.forEach((candidate) => {
tmpHistogram[candidate] += 1
})

this.contentLengthHistogram = tmpHistogram
// Query type
if (stats.pathname && stats.pathname !== '/') {
this.totalResponsesByQueryType['CID+PATH'] += 1
} else {
this.totalResponsesByQueryType['CID'] += 1
}
}

/**
* @param {FetchStats} stats
*/
_updateResponseTimeHistogram(stats) {
const tmpHistogram = {
...this.responseTimeHistogram,
}

// Get all the histogram buckets where the response time is smaller
const histogramCandidates = responseTimeHistogram.filter(
(h) => stats.responseTime < h
this.responseTimeHistogram = getUpdatedHistogram(
this.responseTimeHistogram,
responseTimeHistogram,
stats.responseTime
)
}
}

histogramCandidates.forEach((candidate) => {
tmpHistogram[candidate] += 1
function getUpdatedHistogram(histogramData, histogramBuckets, value) {
const updatedHistogram = {
...histogramData,
}
// Update all the histogram buckets where the response time is smaller
histogramBuckets
.filter((h) => value < h)
.forEach((candidate) => {
updatedHistogram[candidate] += 1
})

this.responseTimeHistogram = tmpHistogram
}
return updatedHistogram
}

/**
* @return {Record<QueryType, number>}
*/
function createResponsesByQueryTypeObject() {
const e = queryType.map((t) => [t, 0])
return Object.fromEntries(e)
}

function createContentLengthHistogramObject() {
const h = contentLengthHistogram.map((h) => [h, 0])
return Object.fromEntries(h)
}

// Either CID is stored in NFT.storage or not
export const queryType = ['CID', 'CID+PATH']

// We will count occurences per bucket where content size is less or equal than bucket value
export const contentLengthHistogram = [
0.5, 1, 2, 5, 25, 50, 100, 500, 1000, 5000, 10000, 15000, 20000, 30000, 32000,
Expand Down
63 changes: 46 additions & 17 deletions packages/gateway/src/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,25 @@ import {
*/
export async function gatewayGet(request, env, ctx) {
const startTs = Date.now()
const reqUrl = new URL(request.url)
const cid = getCidFromSubdomainUrl(reqUrl)
const pathname = reqUrl.pathname

const cache = caches.default
let res = await cache.match(request.url)

if (res) {
// Update cache metrics in background
const responseTime = Date.now() - startTs

ctx.waitUntil(updateSummaryCacheMetrics(request, env, res, responseTime))
ctx.waitUntil(
updateSummaryCacheMetrics(request, env, res, responseTime, {
pathname,
})
)
return res
}

const reqUrl = new URL(request.url)
const cid = getCidFromSubdomainUrl(reqUrl)
const pathname = reqUrl.pathname

// Prepare IPFS gateway requests
const shouldPreventRateLimit = await getGatewayRateLimitState(request, env)
const gatewayReqs = env.ipfsGateways.map((gwUrl) =>
Expand Down Expand Up @@ -91,7 +95,9 @@ export async function gatewayGet(request, env, ctx) {
)

await Promise.all([
storeWinnerGwResponse(request, env, winnerGwResponse),
storeWinnerGwResponse(request, env, winnerGwResponse, {
pathname,
}),
settleGatewayRequests(),
// Cache request URL in Cloudflare CDN if smaller than CF_CACHE_MAX_OBJECT_SIZE
contentLengthMb <= CF_CACHE_MAX_OBJECT_SIZE &&
Expand Down Expand Up @@ -120,7 +126,8 @@ export async function gatewayGet(request, env, ctx) {
updateGatewayMetrics(request, env, r.value, false)
)
)
wasRateLimited && updateGatewayRedirectCounter(request, env)
// Update redirect counter
wasRateLimited && (await updateGatewayRedirectCounter(request, env))
})()
)

Expand Down Expand Up @@ -156,12 +163,19 @@ export async function gatewayGet(request, env, ctx) {
*
* @param {Request} request
* @param {Env} env
* @param {GatewayResponse} winnerGwResponse
* @param {GatewayResponse} gwResponse
* @param {Object} [options]
* @param {string} [options.pathname]
*/
async function storeWinnerGwResponse(request, env, winnerGwResponse) {
async function storeWinnerGwResponse(
request,
env,
gwResponse,
{ pathname } = {}
) {
await Promise.all([
updateGatewayMetrics(request, env, winnerGwResponse, true),
updateSummaryWinnerMetrics(request, env, winnerGwResponse),
updateGatewayMetrics(request, env, gwResponse, true),
updateSummaryWinnerMetrics(request, env, { gwResponse, pathname }),
])
}

Expand Down Expand Up @@ -245,16 +259,24 @@ function getHeaders(request) {
* @param {import('./env').Env} env
* @param {Response} response
* @param {number} responseTime
* @param {Object} [options]
* @param {string} [options.pathname]
*/
async function updateSummaryCacheMetrics(request, env, response, responseTime) {
// Get durable object for summary
async function updateSummaryCacheMetrics(
request,
env,
response,
responseTime,
{ pathname } = {}
) {
const id = env.summaryMetricsDurable.idFromName(SUMMARY_METRICS_ID)
const stub = env.summaryMetricsDurable.get(id)

/** @type {import('./durable-objects/summary-metrics').FetchStats} */
const contentLengthStats = {
contentLength: Number(response.headers.get('content-length')),
responseTime,
pathname,
}

await stub.fetch(
Expand Down Expand Up @@ -294,17 +316,24 @@ async function getGatewayRateLimitState(request, env) {
/**
* @param {Request} request
* @param {import('./env').Env} env
* @param {GatewayResponse} gwResponse
* @param {Object} options
* @param {GatewayResponse} [options.gwResponse]
* @param {string} [options.pathname]
*/
async function updateSummaryWinnerMetrics(request, env, gwResponse) {
async function updateSummaryWinnerMetrics(
request,
env,
{ gwResponse, pathname }
) {
// Get durable object for gateway
const id = env.summaryMetricsDurable.idFromName(SUMMARY_METRICS_ID)
const stub = env.summaryMetricsDurable.get(id)

/** @type {import('./durable-objects/summary-metrics').FetchStats} */
const fetchStats = {
responseTime: gwResponse.responseTime,
contentLength: Number(gwResponse.response.headers.get('content-length')),
contentLength: Number(gwResponse?.response.headers.get('content-length')),
responseTime: gwResponse?.responseTime,
pathname,
}

await stub.fetch(getDurableRequestUrl(request, 'metrics/winner', fetchStats))
Expand Down
12 changes: 12 additions & 0 deletions packages/gateway/src/metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ export async function metricsGet(request, env, ctx) {
`# HELP nftgateway_redirect_total Total redirects to gateway.`,
`# TYPE nftgateway_redirect_total counter`,
`nftgateway_redirect_total{env="${env.ENV}"} ${metricsCollected.gatewayRedirectCount}`,
`# HELP nftgateway_responses_by_query_type_total total of responses by query status. Either CID or CID+PATH.`,
`# TYPE nftgateway_responses_by_query_type_total counter`,
Object.keys(metricsCollected.summaryMetrics.totalResponsesByQueryType)
.map(
(type) =>
`nftgateway_responses_by_query_type_total{env="${
env.ENV
}",type="${type}"} ${
metricsCollected.summaryMetrics.totalResponsesByQueryType[type] || 0
}`
)
.join('\n'),
].join('\n')

res = new Response(metrics, {
Expand Down
Loading

0 comments on commit 2f2d3c8

Please sign in to comment.