1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-31 01:16:01 +02:00
unleash.unleash/src/lib/routes/controller.ts
Gastón Fournier e8e1d6e9f0
fix: path metric labels (#6400)
## About the changes
Some of our metrics are not labeled correctly, one example is
`<base-path>/api/frontend/client/metrics` is labeled as
`/client/metrics`. We can see that in internal-backstage/prometheus:

![image](https://github.com/Unleash/unleash/assets/455064/0d8f1f40-8b5b-49d4-8a88-70b523e9be09)

This issue affects all endpoints that fail to validate the request body.
Also, endpoints that are rejected by the authorization-middleware or the
api-token-middleware are reported as `(hidden)`.

To gain more insights on our api usage but being protective of metrics
cardinality we're prefixing `(hidden)` with some well known base urls:
https://github.com/Unleash/unleash/pull/6400/files#diff-1ed998ca46ffc97c9c0d5d400bfd982dbffdb3004b78a230a8a38e7644eee9b6R17-R33

## How to reproduce:
Make an invalid call to metrics (e.g. stop set to null), then check
/internal-backstage/prometheus and find the 400 error. Expected to be at
`path="/api/client/metrics"` but will have `path=""`:
```shell
curl -H"Authorization: *:development.unleash-insecure-client-api-token" -H'Content-type: application/json' localhost:4242/api/client/metrics -d '{
  "appName": "bash-test",
  "instanceId": "application-name-dacb1234",
  "environment": "development",
  "bucket": {
    "start": "2023-07-27T11:23:44Z",
    "stop": null,
    "toggles": {
      "myCoolToggle": {
        "yes": 25,
        "no": 42,
        "variants": {
          "blue": 6,
          "green": 15,
          "red": 46
        }
      },
      "myOtherToggle": {
        "yes": 0,
        "no": 100
      }
    }
  }
}'
```
2024-03-05 15:25:06 +01:00

222 lines
5.6 KiB
TypeScript

import { IRouter, Router, Request, Response, RequestHandler } from 'express';
import { Logger } from '../logger';
import { IUnleashConfig, NONE } from '../types';
import { handleErrors } from './util';
import requireContentType from '../middleware/content_type_checker';
import { PermissionError } from '../error';
import { storeRequestedRoute } from '../middleware/response-time-metrics';
interface IRequestHandler<
P = any,
ResBody = any,
ReqBody = any,
ReqQuery = any,
> {
(
req: Request<P, ResBody, ReqBody, ReqQuery>,
res: Response<ResBody>,
): Promise<void> | void;
}
type Permission = string | string[];
interface IRouteOptionsBase {
path: string;
permission: Permission;
middleware?: RequestHandler[];
handler: IRequestHandler;
acceptedContentTypes?: string[];
}
interface IRouteOptionsGet extends IRouteOptionsBase {
method: 'get';
}
interface IRouteOptionsNonGet extends IRouteOptionsBase {
method: 'post' | 'put' | 'patch' | 'delete';
acceptAnyContentType?: boolean;
}
type IRouteOptions = IRouteOptionsNonGet | IRouteOptionsGet;
const checkPermission =
(permission: Permission = []) =>
async (req, res, next) => {
const permissions = (
Array.isArray(permission) ? permission : [permission]
).filter((p) => p !== NONE);
if (!permissions.length) {
return next();
}
if (req.checkRbac && (await req.checkRbac(permissions))) {
return next();
}
return res.status(403).json(new PermissionError(permissions)).end();
};
const checkPrivateProjectPermissions = () => async (req, res, next) => {
if (
!req.checkPrivateProjectPermissions ||
(await req.checkPrivateProjectPermissions())
) {
return next();
}
return res.status(404).end();
};
/**
* Base class for Controllers to standardize binding to express Router.
*
* This class will take care of the following:
* - try/catch inside RequestHandler
* - await if the RequestHandler returns a promise.
* - access control
*/
export default class Controller {
private ownLogger: Logger;
app: IRouter;
config: IUnleashConfig;
constructor(config: IUnleashConfig) {
this.ownLogger = config.getLogger(
`controller/${this.constructor.name}`,
);
this.app = Router();
this.config = config;
}
private useRouteErrorHandler(handler: IRequestHandler): IRequestHandler {
return async (req: Request, res: Response) => {
try {
await handler(req, res);
} catch (error) {
handleErrors(res, this.ownLogger, error);
}
};
}
private useContentTypeMiddleware(options: IRouteOptions): RequestHandler[] {
const { middleware = [], acceptedContentTypes = [] } = options;
return options.method === 'get' || options.acceptAnyContentType
? middleware
: [requireContentType(...acceptedContentTypes), ...middleware];
}
route(options: IRouteOptions): void {
this.app[options.method](
options.path,
storeRequestedRoute,
checkPermission(options.permission),
checkPrivateProjectPermissions(),
this.useContentTypeMiddleware(options),
this.useRouteErrorHandler(options.handler.bind(this)),
);
}
get(
path: string,
handler: IRequestHandler,
permission: Permission = NONE,
): void {
this.route({
method: 'get',
path,
handler,
permission,
});
}
post(
path: string,
handler: IRequestHandler,
permission: Permission = NONE,
...acceptedContentTypes: string[]
): void {
this.route({
method: 'post',
path,
handler,
permission,
acceptedContentTypes,
});
}
put(
path: string,
handler: IRequestHandler,
permission: Permission = NONE,
...acceptedContentTypes: string[]
): void {
this.route({
method: 'put',
path,
handler,
permission,
acceptedContentTypes,
});
}
patch(
path: string,
handler: IRequestHandler,
permission: Permission = NONE,
...acceptedContentTypes: string[]
): void {
this.route({
method: 'patch',
path,
handler,
permission,
acceptedContentTypes,
});
}
delete(
path: string,
handler: IRequestHandler,
permission: Permission = NONE,
): void {
this.route({
method: 'delete',
path,
handler,
permission,
acceptAnyContentType: true,
});
}
fileupload(
path: string,
filehandler: IRequestHandler,
handler: Function,
permission: Permission = NONE,
): void {
this.app.post(
path,
storeRequestedRoute,
checkPermission(permission),
checkPrivateProjectPermissions(),
filehandler.bind(this),
this.useRouteErrorHandler(handler.bind(this)),
);
}
use(path: string, router: IRouter): void {
this.app.use(path, router);
}
useWithMiddleware(path: string, router: IRouter, middleware: any): void {
this.app.use(path, middleware, router);
}
get router(): IRouter {
return this.app;
}
}
module.exports = Controller;