• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / storage / 21516573288

30 Jan 2026 12:56PM UTC coverage: 75.548% (-0.4%) from 75.944%
21516573288

push

github

web-flow
feat: otel metrics (#819)

2112 of 3066 branches covered (68.88%)

Branch coverage included in aggregate %.

673 of 1285 new or added lines in 33 files covered. (52.37%)

2 existing lines in 2 files now uncovered.

25902 of 34015 relevant lines covered (76.15%)

94.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.51
/src/http/plugins/log-request.ts
1
import fastifyPlugin from 'fastify-plugin'
1✔
2
import { logger, logSchema, redactQueryParamFromRequest } from '@internal/monitoring'
1✔
3
import { trace } from '@opentelemetry/api'
1✔
4
import { FastifyRequest } from 'fastify/types/request'
1✔
5
import { FastifyReply } from 'fastify/types/reply'
1✔
6

1✔
7
interface RequestLoggerOptions {
1✔
8
  excludeUrls?: string[]
1✔
9
}
1✔
10

1✔
11
declare module 'fastify' {
1✔
12
  interface FastifyRequest {
1✔
13
    executionError?: Error
1✔
14
    operation?: { type: string }
1✔
15
    resources?: string[]
1✔
16
    startTime: number
1✔
17
    executionTime?: number
1✔
18
  }
1✔
19

1✔
20
  interface FastifyContextConfig {
1✔
21
    operation?: { type: string }
1✔
22
    resources?: (req: FastifyRequest<any>) => string[]
1✔
23
    logMetadata?: (req: FastifyRequest<any>) => Record<string, unknown>
1✔
24
  }
1✔
25
}
1✔
26

1✔
27
/**
1✔
28
 * Request logger plugin
1✔
29
 * @param options
1✔
30
 */
1✔
31
export const logRequest = (options: RequestLoggerOptions) =>
1✔
32
  fastifyPlugin(
129✔
33
    async (fastify) => {
129✔
34
      fastify.addHook('onRequest', async (req, res) => {
128✔
35
        req.startTime = Date.now()
138✔
36

138✔
37
        res.raw.once('close', () => {
138✔
38
          if (req.raw.aborted) {
138!
39
            doRequestLog(req, {
×
40
              excludeUrls: options.excludeUrls,
×
41
              statusCode: 'ABORTED REQ',
×
42
              responseTime: (Date.now() - req.startTime) / 1000,
×
43
            })
×
44
            return
×
45
          }
×
46

138✔
47
          if (!res.raw.writableFinished) {
138✔
48
            doRequestLog(req, {
138✔
49
              excludeUrls: options.excludeUrls,
138✔
50
              statusCode: 'ABORTED RES',
138✔
51
              responseTime: (Date.now() - req.startTime) / 1000,
138✔
52
            })
138✔
53
          }
138✔
54
        })
138✔
55
      })
128✔
56

128✔
57
      /**
128✔
58
       * Adds req.resources and req.operation to the request object
128✔
59
       */
128✔
60
      fastify.addHook('preHandler', async (req) => {
128✔
61
        const resourceFromParams = Object.values(req.params || {}).join('/')
123!
62
        const resources = getFirstDefined<string[]>(
123✔
63
          req.resources,
123✔
64
          req.routeOptions.config.resources?.(req),
123✔
65
          (req.raw as any).resources,
123✔
66
          resourceFromParams ? [resourceFromParams] : ([] as string[])
123✔
67
        )
123✔
68

123✔
69
        if (resources && resources.length > 0) {
123✔
70
          resources.map((resource, index) => {
123✔
71
            if (!resource.startsWith('/')) {
40,143✔
72
              resources[index] = `/${resource}`
40,143✔
73
            }
40,143✔
74
          })
123✔
75
        }
123✔
76

123✔
77
        req.resources = resources
123✔
78
        req.operation = req.routeOptions.config.operation
123✔
79

123✔
80
        if (req.operation) {
123✔
81
          trace.getActiveSpan()?.setAttribute('http.operation', req.operation.type)
123!
82
        }
123✔
83
      })
128✔
84

128✔
85
      fastify.addHook('onSend', async (req, _, payload) => {
128✔
86
        req.executionTime = Date.now() - req.startTime
138✔
87
        return payload
138✔
88
      })
128✔
89

128✔
90
      fastify.addHook('onResponse', async (req, reply) => {
128✔
91
        doRequestLog(req, {
138✔
92
          reply,
138✔
93
          excludeUrls: options.excludeUrls,
138✔
94
          statusCode: reply.statusCode,
138✔
95
          responseTime: reply.elapsedTime,
138✔
96
          executionTime: req.executionTime,
138✔
97
        })
138✔
98
      })
128✔
99
    },
128✔
100
    { name: 'log-request' }
129✔
101
  )
1✔
102

1✔
103
interface LogRequestOptions {
1✔
104
  reply?: FastifyReply
1✔
105
  excludeUrls?: string[]
1✔
106
  statusCode: number | 'ABORTED REQ' | 'ABORTED RES'
1✔
107
  responseTime: number
1✔
108
  executionTime?: number
1✔
109
}
1✔
110

1✔
111
function doRequestLog(req: FastifyRequest, options: LogRequestOptions) {
276✔
112
  if (options.excludeUrls?.includes(req.url)) {
276!
113
    return
×
114
  }
×
115

276✔
116
  const rMeth = req.method
276✔
117
  const rUrl = redactQueryParamFromRequest(req, [
276✔
118
    'token',
276✔
119
    'X-Amz-Credential',
276✔
120
    'X-Amz-Signature',
276✔
121
    'X-Amz-Security-Token',
276✔
122
  ])
276✔
123
  const uAgent = req.headers['user-agent']
276✔
124
  const rId = req.id
276✔
125
  const cIP = req.ip
276✔
126
  const statusCode = options.statusCode
276✔
127
  const error = (req.raw as any).executionError || req.executionError
276✔
128
  const tenantId = req.tenantId
276✔
129

276✔
130
  let reqMetadata: Record<string, unknown> = {}
276✔
131

276✔
132
  if (req.routeOptions.config.logMetadata) {
276✔
133
    try {
18✔
134
      reqMetadata = req.routeOptions.config.logMetadata(req)
18✔
135

18✔
136
      if (reqMetadata) {
18✔
137
        try {
18✔
138
          trace.getActiveSpan()?.setAttribute('http.metadata', JSON.stringify(reqMetadata))
18!
139
        } catch (e) {
18!
NEW
140
          // do nothing
×
NEW
141
          logSchema.warning(logger, 'Failed to serialize log metadata', {
×
NEW
142
            type: 'otel',
×
NEW
143
            error: e,
×
NEW
144
          })
×
NEW
145
        }
×
146
      }
18✔
147
    } catch (e) {
18!
NEW
148
      logSchema.error(logger, 'Failed to get log metadata', {
×
NEW
149
        type: 'request',
×
NEW
150
        error: e,
×
NEW
151
      })
×
NEW
152
    }
×
153
  }
18✔
154

276✔
155
  const buildLogMessage = `${tenantId} | ${rMeth} | ${statusCode} | ${cIP} | ${rId} | ${rUrl} | ${uAgent}`
276✔
156

276✔
157
  logSchema.request(req.log, buildLogMessage, {
276✔
158
    type: 'request',
276✔
159
    req,
276✔
160
    reqMetadata,
276✔
161
    res: options.reply,
276✔
162
    responseTime: options.responseTime,
276✔
163
    executionTime: options.executionTime,
276✔
164
    error: error,
276✔
165
    owner: req.owner,
276✔
166
    role: req.jwtPayload?.role,
276✔
167
    resources: req.resources,
276✔
168
    operation: req.operation?.type ?? req.routeOptions.config.operation?.type,
276✔
169
    serverTimes: req.serverTimings,
276✔
170
  })
276✔
171
}
276✔
172

1✔
173
function getFirstDefined<T>(...values: any[]): T | undefined {
123✔
174
  for (const value of values) {
123✔
175
    if (value !== undefined) {
440✔
176
      return value
123✔
177
    }
123✔
178
  }
440✔
179
  return undefined
×
180
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc