Connect a LoopBack 4 app
Add ApiTraffic to a LoopBack 4 API as a component: install, register, credentials, what is recorded, firewall, APM and troubleshooting.
@apitraffic/loopback is a LoopBack 4 component. Register it once, in your application’s
constructor, and every request your REST server serves is recorded and checked by the ApiTraffic
firewall, whichever sequence your application uses.
It is @apitraffic/node, the one-install SDK, wired into LoopBack: the
same capture, the same firewall and APM bridge, the same helpers, all enabled from your ApiTraffic
account rather than in code.
Installation
npm
npm install @apitraffic/loopback
yarn
yarn add @apitraffic/loopback
pnpm
pnpm add @apitraffic/loopback
Note: Node 20+ and
@loopback/rest13, 14 or 15 (@loopback/core5, 6 or 7) required.
Basic Setup
// src/application.ts
import { ApplicationConfig } from '@loopback/core';
import { RestApplication } from '@loopback/rest';
import { ApiTrafficBindings, ApiTrafficComponent, type ApiTrafficOptions } from '@apitraffic/loopback';
export class MyApplication extends RestApplication {
constructor(options: ApplicationConfig = {}) {
super(options);
this.configure<ApiTrafficOptions>(ApiTrafficBindings.COMPONENT).to({
token: process.env.API_TRAFFIC_TOKEN,
bucket: process.env.API_TRAFFIC_BUCKET,
});
this.component(ApiTrafficComponent);
// ...your controllers, sequence, other components
}
}
Configure the component before registering it: LoopBack builds a component as soon as it is
registered. Without a configuration every option falls back to its API_TRAFFIC_* environment
variable, so this.component(ApiTrafficComponent) alone is a complete setup.
It works with any basePath, with the default middleware sequence and with the action-based
DefaultSequence older applications were generated with, on every REST server the application has,
and in an application mounted on an Express app of your own (expressApp.use('/api', lbApp.requestHandler)). Mounted under a path with parameters (/tenants/:tenantId), or through an
Express Router, requests are recorded without the route, and the endpoint is worked out from the
path.
What is recorded
Every request, including the ones a controller never sees. The component runs ahead of LoopBack’s whole sequence (its CORS handling, middleware, routing, authentication and body parsing), so these are all recorded, with their final status and whatever your code tagged on the way:
- a request authentication or a middleware rejects or answers itself (
401), - a request an interceptor rejects (
@loopback/authorization’s403), - a request whose parameters fail validation (
400,422), - a request no route matches (
404), including one outside the REST server’sbasePath, - a request whose controller throws, as the response LoopBack’s reject action sent.
The route LoopBack matched is sent with each request, controller base path and REST basePath
included, so @get('/users/{id}') in a controller with @api({ basePath: '/v1' }) on a server
with basePath: '/api' is the endpoint /api/v1/users/{id}. See
Endpoint Handling. Requests served by an Express router
you mounted with mountExpressRouter(), or by app.static(), are recorded without one, and the
endpoint is worked out from the path.
Requests are captured by @apitraffic/express on the REST server’s Express application: exactly
what the Express page describes under “What is captured” holds here
too.
Tagging, tracing and spans
The helpers work anywhere a request is being handled: controllers, services, repositories, interceptors, middleware and sequence actions.
import { Interceptor } from '@loopback/core';
import { RestBindings } from '@loopback/rest';
import { tag } from '@apitraffic/loopback';
export const accountTag: Interceptor = async (invocationCtx, next) => {
const request = await invocationCtx.get(RestBindings.Http.REQUEST);
// Searchable on this request, whether or not it gets any further.
tag('accountId', String(request.headers['x-account-id']));
return next();
};
import { repository } from '@loopback/repository';
import { span, trace } from '@apitraffic/loopback';
import { UserRepository } from '../repositories';
export class UserService {
constructor(@repository(UserRepository) private users: UserRepository) {}
async findOne(id: string) {
trace(`Loading user ${id}`);
return span('db.query', 'load user', () => this.users.findById(id));
}
}
Outside a request (a cron job, a queue consumer) the helpers do nothing and never throw.
stats() reports the SDK’s delivery health (delivered, queued, dropped), for a health endpoint or
a log line.
Shutdown
app.stop() (which the application generated by lb4 calls on SIGTERM and SIGINT when
shutdown.signals is set) delivers what was captured and releases the SDK’s timers and
connections once the last application using the component has stopped. The REST server has
stopped by then, so every request it served has been reported. An application started again
afterwards sets the SDK up again and is recorded as before; the APM bridge is not restarted.
Firewall
The ApiTraffic firewall is mounted by the component, and whether it runs is decided in your ApiTraffic account: nothing is loaded until you switch a policy on, and a policy change reaches every server without a deploy. It runs twice on a request at most, and counts it once:
- ahead of the sequence, right after the request is recorded, so a request it blocks there never reaches LoopBack’s middleware, routing or authentication;
- once LoopBack has parsed the body, so body rules see it: with the default middleware sequence,
between parameter parsing and the method, ahead of every interceptor; with the action-based
DefaultSequence, as a global interceptor in the groupapitraffic.firewall(list it first in your global interceptor order to run it ahead of the others). It is evaluated again there only if it had no body before.
Blocked requests are recorded with the policy’s status and tagged waf.*.
To refuse it locally whatever the dashboard says (change control, or an incident switch that does
not depend on the control plane), set waf: false or API_TRAFFIC_WAF_ENABLED=false:
this.configure<ApiTrafficOptions>(ApiTrafficBindings.COMPONENT).to({ waf: false });
See Firewall setup for policies and rules.
Application Performance Monitoring (APM)
The APM bridge captures database, cache, queue, GraphQL, gRPC, and fetch/undici spans within
each ApiTraffic request. Install the OpenTelemetry peers it uses:
npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @opentelemetry/auto-instrumentations-node
OpenTelemetry patches modules as they are loaded, so it has to start before LoopBack and your datasource connectors do. Preload it:
node -r @apitraffic/loopback/register .
or import it on the very first line of src/index.ts, ahead of every other import:
import '@apitraffic/loopback/register';
(Not register() from @apitraffic/loopback itself: importing the component loads LoopBack’s REST
stack, which then loads before the bridge can patch it.)
If your application already configures OpenTelemetry (with @loopback/opentelemetry, say), add
ApiTrafficSpanProcessor from @apitraffic/apm to that provider instead, so spans are not
double-instrumented.
Configuration Options
| Option | Environment Variable | Required | Type | Description |
|---|---|---|---|---|
token |
API_TRAFFIC_TOKEN |
Yes | String | Ingest token from your ApiTraffic account |
bucket |
API_TRAFFIC_BUCKET |
Yes | String | Bucket ID for data organization |
interceptOutbound |
API_TRAFFIC_INTERCEPT_OUTBOUND |
No | Boolean | Monitor outbound HTTP requests (default: true) |
debug |
API_TRAFFIC_DEBUG |
No | Boolean | Enable debug logging (default: false) |
waf |
API_TRAFFIC_WAF_ENABLED |
No | Boolean or object | false refuses the firewall locally; an object passes settings to @apitraffic/waf. Normally left unset: the firewall is switched on from your account |
An option passed in code wins over the environment.
The options are typed: with configure(), a value of the wrong type is a
compile error, and so is a misspelled key in an object literal.
Embed tokens
The embeddable request log authenticates with a token your server
mints. embedToken is exported here, as from every ApiTraffic package:
import { get } from '@loopback/rest';
import { embedToken } from '@apitraffic/loopback';
export class EmbedController {
@get('/embed/token')
token() {
return embedToken({ filters: { tags: { customerId: 'cus_123' } }, ttl: 900 });
}
}
Troubleshooting
No requests are recorded
Check that this.component(ApiTrafficComponent) runs in the application you start, and that
API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET are set. Set debug: true to see what the SDK is
doing.
The options I configured are ignored
this.configure(ApiTrafficBindings.COMPONENT) has to come before
this.component(ApiTrafficComponent): the component reads its configuration when it is
registered.
Tags set in a cron job or queue consumer are missing
Tags, traces and spans belong to an HTTP request. Code that does not run while a request is being handled has none to attach them to, and the helpers do nothing there.
I already mount @apitraffic/node with expressMiddleware()
Remove it when you register the component. Left in place it does no harm (each request is
still recorded and evaluated once), but the component records more of your traffic than it can:
expressMiddleware() runs inside the sequence, after a request outside the basePath has
already been answered.
Sample Application
A complete working example is available in the GitHub repository.