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

[frontend] document instrumentation #444

Merged
merged 5 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/service_table.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ View [Service Graph](../README.md#architecture) to visualize request flows.
| [currencyservice](../src/currencyservice/README.md) | C++ | Converts one money amount to another currency. Uses real values fetched from European Central Bank. It's the highest QPS service. |
| [emailservice](./services/emailservice.md) | Ruby | Sends users an order confirmation email (mock). |
| [featureflagservice](./services/featureflagservice.md) | Erlang/Elixir | CRUD feature flag service to demonstrate various scenarios like fault injection & how to emit telemetry from a feature flag reliant service. |
| [frontend](../src/frontend/README.md) | JavaScript | Exposes an HTTP server to serve the website. Does not require signup/login and generates session IDs for all users automatically. |
| [frontend](./services/frontend.md) | JavaScript | Exposes an HTTP server to serve the website. Does not require signup/login and generates session IDs for all users automatically. |
| [loadgenerator](./services/loadgenerator.md) | Python/Locust | Continuously sends requests imitating realistic user shopping flows to the frontend. |
| [paymentservice](./services/paymentservice.md) | JavaScript | Charges the given credit card info (mock) with the given amount and returns a transaction ID. |
| [productcatalogservice](./services/productcatalogservice.md) | Go | Provides the list of products from a JSON file and ability to search products and get individual products. |
Expand Down
174 changes: 174 additions & 0 deletions docs/services/frontend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# frontend

The frontend is responsible to provide a UI for users, as well
as an API leveraged by the UI or other clients. The application is based on
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the first sentence, there could be a short, high-level description of the two parts of the instrumentation: browser side and the backend.

[Next.JS](https://nextjs.org/) to provide a React web-based UI and API routes.

[Frontend source](../../src/frontend/)

## SDK initialization

It is recommended to use a Node required module when starting your NodeJS
application to initialize the SDK and auto-instrumentation. When initializing
the OpenTelemetry NodeJS SDK, you optionally specify which auto-instrumentation
libraries to leverage, or make use of the `getNodeAutoInstrumentations()`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two options are described. It could be added that which one we selected for the demo app and why.

function which includes most popular frameworks. The `utils/telemetry/Instrumentation.js`
file contains all code required to initialize the SDK and auto-instrumentation
based on standard [OpenTelemetry environment variables](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md)
for OTLP export, resource attributes, and service name.

```javascript
const opentelemetry = require("@opentelemetry/sdk-node")
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node")
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc')

const sdk = new opentelemetry.NodeSDK({
traceExporter: new OTLPTraceExporter(),
instrumentations: [ getNodeAutoInstrumentations() ]
})

sdk.start()
```

Node required modules are loaded using the `--require` command line argument.
This can be done in the `scripts.start` section of `package.json` and starting
the application using `npm start`.

```json
"scripts": {
"start": "node --require ./Instrumentation.js server.js",
},
```

## Traces

### Span Exceptions and status

You can use the span object's `recordException` function to create a span event
with the full stack trace of a handled error. When recording an exception also
be sure to set the span's status accordingly. You can see this in the catch
block of the `NextApiHandler` function in the `utils/telemetry/InstrumentationMiddleware.ts`
file.

```typescript
span.recordException(error as Exception);
span.setStatus({code: SpanStatusCode.ERROR});
```

### Create new spans

New spans can be created and started using `Tracer.startSpan("spanName", options)`.
Several options can be used to specify how the span can be created.

- `root: true` will create a new trace, setting this span as the root.
- `links` are used to specify links to other spans (even within another trace)
that should be referenced.
- `attributes` are key/value pairs added to a span, typically used for
application context.

```typescript
span = tracer.startSpan(`HTTP ${method}`, {
root: true,
kind: SpanKind.SERVER,
links: [{context: syntheticSpan.spanContext()}],
attributes: {
"app.synthetic_request": true,
[SemanticAttributes.HTTP_TARGET]: target,
[SemanticAttributes.HTTP_STATUS_CODE]: response.statusCode,
[SemanticAttributes.HTTP_ROUTE]: url,
[SemanticAttributes.HTTP_METHOD]: method,
[SemanticAttributes.HTTP_USER_AGENT]: headers['user-agent'] || '',
[SemanticAttributes.HTTP_URL]: `${headers.host}${url}`,
[SemanticAttributes.HTTP_FLAVOR]: httpVersion,
}
});
```

## Browser Instrumentation

The web-based UI that the frontend provides is also instrumented for web
browsers. OpenTelemetry instrumentation is included as part of the Next.js App
component in `pages/_app.tsx`. Here instrumentation is imported and
initialized.

```typescript
import FrontendTracer from '../utils/telemetry/FrontendTracer';

if (typeof window !== 'undefined') FrontendTracer();
```

The `utils/telemetry/FrontendTracer.ts` file contains code to intialize a
TracerProvider, establish an OTLP export, register trace context propagators,
and register web specific auto-instrumentation libraries. Since the browser
will send data to an OpenTelemetry collector that will likely be on a separate
domain, CORS headers are also setup accordingly.

```typescript
import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from '@opentelemetry/core';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const FrontendTracer = async () => {
const { ZoneContextManager } = await import('@opentelemetry/context-zone');

const provider = new WebTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME,
}),
});

provider.addSpanProcessor(new SimpleSpanProcessor(new OTLPTraceExporter()));

const contextManager = new ZoneContextManager();

provider.register({
contextManager,
propagator: new CompositePropagator({
propagators: [new W3CBaggagePropagator(), new W3CTraceContextPropagator()],
}),
});

registerInstrumentations({
tracerProvider: provider,
instrumentations: [
getWebAutoInstrumentations({
'@opentelemetry/instrumentation-fetch': {
propagateTraceHeaderCorsUrls: /.*/,
clearTimingResources: true,
},
}),
],
});
};

export default FrontendTracer;
```

## Metrics

TBD

## Logs

TBD

## Baggage

OpenTelemetry Baggage is leveraged in the frontend to check if the request is
synthetic (from the load generator). Synthetic requests will force the creation
of a new trace. The root span from the new trace will contain many of the same
attributes as an HTTP request instrumented span.

To determine if a Baggage item is set, you can leverage the `propagation` API
to parse the Baggage header, and leverage the `baggage` API to get or
set entries.

```typescript
const baggage = propagation.getBaggage(context.active());
if (baggage?.getEntry("synthetic_request")?.value == "true") {...}
```
26 changes: 0 additions & 26 deletions src/frontend/utils/telemetry/BackendTracer.ts

This file was deleted.

21 changes: 7 additions & 14 deletions src/frontend/utils/telemetry/Instrumentation.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
const { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } = require('@opentelemetry/core');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { NodeSDK, api } = require('@opentelemetry/sdk-node');
const opentelemetry = require("@opentelemetry/sdk-node")
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node")
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc')

api.propagation.setGlobalPropagator(
new CompositePropagator({
propagators: [new W3CBaggagePropagator(), new W3CTraceContextPropagator()],
})
);

const sdk = new NodeSDK({
const sdk = new opentelemetry.NodeSDK({
traceExporter: new OTLPTraceExporter(),
instrumentations: getNodeAutoInstrumentations(),
});
instrumentations: [ getNodeAutoInstrumentations() ]
})

sdk.start();
sdk.start()
22 changes: 17 additions & 5 deletions src/frontend/utils/telemetry/InstrumentationMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {NextApiHandler} from 'next';
import Tracer from './BackendTracer';
import {context, Exception, propagation, SpanKind, SpanStatusCode, trace} from '@opentelemetry/api';
import {context, Exception, propagation, Span, SpanKind, SpanStatusCode, trace} from '@opentelemetry/api';
import {SemanticAttributes} from '@opentelemetry/semantic-conventions';
import {Span} from '@opentelemetry/sdk-trace-base';

const InstrumentationMiddleware = (handler: NextApiHandler): NextApiHandler => {
return async (request, response) => {
Expand All @@ -15,7 +13,8 @@ const InstrumentationMiddleware = (handler: NextApiHandler): NextApiHandler => {
// if synthetic_request baggage is set, create a new trace linked to the span in context
// this span will look similar to the auto-instrumented HTTP span
const syntheticSpan = trace.getSpan(context.active()) as Span;
span = Tracer.getTracer().startSpan(`HTTP ${method}`, {
const tracer = trace.getTracer(process.env.OTEL_SERVICE_NAME as string);
span = tracer.startSpan(`HTTP ${method}`, {
root: true,
kind: SpanKind.SERVER,
links: [{context: syntheticSpan.spanContext()}],
Expand All @@ -37,7 +36,7 @@ const InstrumentationMiddleware = (handler: NextApiHandler): NextApiHandler => {
}

try {
await Tracer.runWithSpan(span, async () => handler(request, response));
await runWithSpan(span, async () => handler(request, response));
} catch (error) {
span.recordException(error as Exception);
span.setStatus({code: SpanStatusCode.ERROR});
Expand All @@ -48,4 +47,17 @@ const InstrumentationMiddleware = (handler: NextApiHandler): NextApiHandler => {
};
};

async function runWithSpan(parentSpan: Span, fn: () => Promise<unknown>) {
const ctx = trace.setSpan(context.active(), parentSpan);

try {
return await context.with(ctx, fn);
} catch (error) {
parentSpan.recordException(error as Exception);
parentSpan.setStatus({ code: SpanStatusCode.ERROR });

throw error;
}
}

export default InstrumentationMiddleware;