Skip to content

Commit 7209bd3

Browse files
authored
Add sample for opentelemetry-js (#1020)
Sample code with callable function instrumented with opentelemetry-js.
1 parent 1e52d3e commit 7209bd3

File tree

6 files changed

+189
-0
lines changed

6 files changed

+189
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Instrumenting Cloud Functions for Firebase with Open Telemetry
2+
This sample demonstrates instrumenting your Cloud Functions for Firebase using [OpenTelemetry](https://opentelemetry.io).
3+
4+
See Firebase Summit 2022 Talk "Observability in Cloud Functions for Firebase" for motivations and context.
5+
6+
Open Telemetry SDK provides both automatic and manual instrumentation, both of which are demonstrated here. See [OpenTelemetry JS documentations](https://opentelemetry.io/docs/instrumentation/js/) for more information about how to use and configure OpenTelemetry for your javascript project.
7+
8+
## Notable Files
9+
* `./tracing.js`: Initializes OpenTelemetry SDK to automatically instrument HTTP/GRPC/Express modules and export the generated traces to Google Cloud Trace.
10+
11+
* `./.env`: Configures `NODE_OPTIONS` to preload the `tracing.js` module. This is important because OpenTelemtry SDK works by monkey-patching instrumented modules and must run first before other module is loaded.
12+
13+
* `./index.js`: Includes sample code for generating custom spans using the OpenTelemetry API. e.g.:
14+
```js
15+
const opentelemetry = require('@opentelemetry/api');
16+
17+
const tracer = opentelemetry.trace.getTracer();
18+
await tracer.startActiveSpan("calculatePrice", async (span) => {
19+
totalUsd = await calculatePrice(productIds);
20+
span.end();
21+
});
22+
```
23+
24+
## Deploy and test
25+
1. Deploy your function using firebase deploy --only functions
26+
2. Seed Firestore with mock data.
27+
3. Send callable request to the deployed function, e.g.:
28+
```
29+
$ curl -X POST -H "content-type: application/json" https:// -d '{ "data": ... }'
30+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"functions": {
3+
"source": "functions"
4+
},
5+
"emulators": {
6+
"functions": {
7+
"port": 5001
8+
},
9+
"firestore": {
10+
"port": 8080
11+
},
12+
"ui": {
13+
"enabled": true
14+
},
15+
"singleProjectMode": true
16+
}
17+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const {onCall} = require("firebase-functions/v2/https");
2+
const logger = require("firebase-functions/logger");
3+
const {initializeApp} = require('firebase-admin/app');
4+
const {getFirestore} = require('firebase-admin/firestore');
5+
const opentelemetry = require('@opentelemetry/api');
6+
const {Timer} = require("./timer");
7+
8+
initializeApp();
9+
const db = getFirestore();
10+
11+
function sliceIntoChunks(arr, chunkSize) {
12+
const res = [];
13+
for (let i = 0; i < arr.length; i += chunkSize) {
14+
const chunk = arr.slice(i, i + chunkSize);
15+
res.push(chunk);
16+
}
17+
return res;
18+
}
19+
20+
async function calculatePrice(productIds) {
21+
const timer = new Timer();
22+
let totalUsd = 0;
23+
const products = await db.getAll(...productIds.map(id => db.doc(`products/${id}`)));
24+
for (const product of products) {
25+
totalUsd += product.data()?.usd || 0;
26+
}
27+
logger.info("calculatePrice", {calcPriceMs: timer.measureMs()});
28+
return totalUsd;
29+
}
30+
31+
async function calculateDiscount(productIds) {
32+
const timer = new Timer();
33+
34+
let discountUsd = 0;
35+
const processConcurrently = sliceIntoChunks(productIds, 10)
36+
.map(async (productIds) => {
37+
const discounts = await db.collection("discounts")
38+
.where("products", "array-contains", productIds)
39+
.get();
40+
for (const discount of discounts.docs) {
41+
discountUsd += discount.data().usd || 0;
42+
}
43+
});
44+
await Promise.all(processConcurrently);
45+
logger.info("calculateDiscount", {calcDiscountMs: timer.measureMs()});
46+
return discountUsd;
47+
}
48+
49+
exports.calculatetotal = onCall(async (req) => {
50+
const {productIds} = req.data;
51+
52+
let totalUsd = 0;
53+
const tracer = opentelemetry.trace.getTracer();
54+
await tracer.startActiveSpan("calculatePrice", async (span) => {
55+
totalUsd = await calculatePrice(productIds);
56+
span.end();
57+
});
58+
await tracer.startActiveSpan("calculateDiscount", async (span) => {
59+
totalUsd -= await calculateDiscount(productIds);
60+
span.end();
61+
});
62+
return {totalUsd};
63+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "functions",
3+
"description": "Cloud Functions for Firebase",
4+
"scripts": {
5+
"lint": "eslint .",
6+
"serve": "firebase emulators:start --only functions",
7+
"shell": "firebase functions:shell",
8+
"start": "npm run shell",
9+
"deploy": "firebase deploy --only functions",
10+
"logs": "firebase functions:log"
11+
},
12+
"engines": {
13+
"node": "16"
14+
},
15+
"dependencies": {
16+
"@google-cloud/opentelemetry-cloud-trace-exporter": "^1.1.0",
17+
"@google-cloud/opentelemetry-cloud-trace-propagator": "^0.14.0",
18+
"@opentelemetry/api": "^1.2.0",
19+
"@opentelemetry/instrumentation": "^0.33.0",
20+
"@opentelemetry/instrumentation-grpc": "^0.33.0",
21+
"@opentelemetry/instrumentation-http": "^0.33.0",
22+
"@opentelemetry/resource-detector-gcp": "^0.27.2",
23+
"@opentelemetry/sdk-node": "^0.33.0",
24+
"firebase-admin": "^10.2.0",
25+
"firebase-functions": "^4.0.0",
26+
"opentelemetry-instrumentation-express": "^0.29.0"
27+
},
28+
"private": true
29+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
exports.Timer = class {
2+
constructor() {
3+
this.start = process.hrtime.bigint();
4+
}
5+
6+
measureMs() {
7+
const duration = process.hrtime.bigint() - this.start;
8+
return (duration / 1_000_000n).toString();
9+
}
10+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const opentelemetry = require("@opentelemetry/sdk-node");
2+
const {TraceExporter} = require("@google-cloud/opentelemetry-cloud-trace-exporter");
3+
const {HttpInstrumentation} = require("@opentelemetry/instrumentation-http");
4+
const {GrpcInstrumentation} = require("@opentelemetry/instrumentation-grpc");
5+
const {ExpressInstrumentation} = require('opentelemetry-instrumentation-express');
6+
const {gcpDetector} = require("@opentelemetry/resource-detector-gcp");
7+
const {
8+
CloudPropagator,
9+
} = require("@google-cloud/opentelemetry-cloud-trace-propagator");
10+
11+
12+
// Only enable OpenTelemetry if the function is actually deployed.
13+
// Emulators don't reflect real-world latency"
14+
if (!process.env.FUNCTIONS_EMULATOR) {
15+
const sdk = new opentelemetry.NodeSDK({
16+
// Setup automatic instrumentation for
17+
// http, grpc, and express modules.
18+
instrumentations: [
19+
new HttpInstrumentation(),
20+
new GrpcInstrumentation(),
21+
new ExpressInstrumentation(),
22+
],
23+
// Make sure opentelemetry know about Cloud Trace http headers
24+
// i.e. 'X-Cloud-Trace-Context'
25+
textMapPropagator: new CloudPropagator(),
26+
// Automatically detect and include span metadata when running
27+
// in GCP, e.g. region of the function.
28+
resourceDetectors: [gcpDetector],
29+
// Export generated traces to Cloud Trace.
30+
traceExporter: new TraceExporter(),
31+
});
32+
33+
sdk.start();
34+
35+
// Ensure that generated traces are exported when the container is
36+
// shutdown.
37+
process.on("SIGTERM", async () => {
38+
await sdk.shutdown();
39+
});
40+
}

0 commit comments

Comments
 (0)