Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions 2nd-gen/instrument-with-opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Instrumenting Cloud Functions for Firebase with Open Telemetry
This sample demonstrates instrumenting your Cloud Functions for Firebase using [OpenTelemetry](https://opentelemetry.io).

See Firebase Summit 2022 Talk "Observability in Cloud Functions for Firebase" for motivations and context.

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.

## Notable Files
* `./tracing.js`: Initializes OpenTelemetry SDK to automatically instrument HTTP/GRPC/Express modules and export the generated traces to Google Cloud Trace.

* `./.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.

* `./index.js`: Includes sample code for generating custom spans using the OpenTelemetry API. e.g.:
```js
const opentelemetry = require('@opentelemetry/api');

const tracer = opentelemetry.trace.getTracer();
await tracer.startActiveSpan("calculatePrice", async (span) => {
totalUsd = await calculatePrice(productIds);
span.end();
});
```

## Deploy and test
1. Deploy your function using firebase deploy --only functions
2. Seed Firestore with mock data.
3. Send callable request to the deployed function, e.g.:
```
$ curl -X POST -H "content-type: application/json" https:// -d '{ "data": ... }'
```
17 changes: 17 additions & 0 deletions 2nd-gen/instrument-with-opentelemetry/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"functions": {
"source": "functions"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"ui": {
"enabled": true
},
"singleProjectMode": true
}
}
63 changes: 63 additions & 0 deletions 2nd-gen/instrument-with-opentelemetry/functions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const {onCall} = require("firebase-functions/v2/https");
const logger = require("firebase-functions/logger");
const {initializeApp} = require('firebase-admin/app');
const {getFirestore} = require('firebase-admin/firestore');
const opentelemetry = require('@opentelemetry/api');
const {Timer} = require("./timer");

initializeApp();
const db = getFirestore();

function sliceIntoChunks(arr, chunkSize) {
const res = [];
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.slice(i, i + chunkSize);
res.push(chunk);
}
return res;
}

async function calculatePrice(productIds) {
const timer = new Timer();
let totalUsd = 0;
const products = await db.getAll(...productIds.map(id => db.doc(`products/${id}`)));
for (const product of products) {
totalUsd += product.data()?.usd || 0;
}
logger.info("calculatePrice", {calcPriceMs: timer.measureMs()});
return totalUsd;
}

async function calculateDiscount(productIds) {
const timer = new Timer();

let discountUsd = 0;
const processConcurrently = sliceIntoChunks(productIds, 10)
.map(async (productIds) => {
const discounts = await db.collection("discounts")
.where("products", "array-contains", productIds)
.get();
for (const discount of discounts.docs) {
discountUsd += discount.data().usd || 0;
}
});
await Promise.all(processConcurrently);
logger.info("calculateDiscount", {calcDiscountMs: timer.measureMs()});
return discountUsd;
}

exports.calculatetotal = onCall(async (req) => {
const {productIds} = req.data;

let totalUsd = 0;
const tracer = opentelemetry.trace.getTracer();
await tracer.startActiveSpan("calculatePrice", async (span) => {
totalUsd = await calculatePrice(productIds);
span.end();
});
await tracer.startActiveSpan("calculateDiscount", async (span) => {
totalUsd -= await calculateDiscount(productIds);
span.end();
});
return {totalUsd};
});
29 changes: 29 additions & 0 deletions 2nd-gen/instrument-with-opentelemetry/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"lint": "eslint .",
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "16"
},
"dependencies": {
"@google-cloud/opentelemetry-cloud-trace-exporter": "^1.1.0",
"@google-cloud/opentelemetry-cloud-trace-propagator": "^0.14.0",
"@opentelemetry/api": "^1.2.0",
"@opentelemetry/instrumentation": "^0.33.0",
"@opentelemetry/instrumentation-grpc": "^0.33.0",
"@opentelemetry/instrumentation-http": "^0.33.0",
"@opentelemetry/resource-detector-gcp": "^0.27.2",
"@opentelemetry/sdk-node": "^0.33.0",
"firebase-admin": "^10.2.0",
"firebase-functions": "^4.0.0",
"opentelemetry-instrumentation-express": "^0.29.0"
},
"private": true
}
10 changes: 10 additions & 0 deletions 2nd-gen/instrument-with-opentelemetry/functions/timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
exports.Timer = class {
constructor() {
this.start = process.hrtime.bigint();
}

measureMs() {
const duration = process.hrtime.bigint() - this.start;
return (duration / 1_000_000n).toString();
}
};
40 changes: 40 additions & 0 deletions 2nd-gen/instrument-with-opentelemetry/functions/tracing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const opentelemetry = require("@opentelemetry/sdk-node");
const {TraceExporter} = require("@google-cloud/opentelemetry-cloud-trace-exporter");
const {HttpInstrumentation} = require("@opentelemetry/instrumentation-http");
const {GrpcInstrumentation} = require("@opentelemetry/instrumentation-grpc");
const {ExpressInstrumentation} = require('opentelemetry-instrumentation-express');
const {gcpDetector} = require("@opentelemetry/resource-detector-gcp");
const {
CloudPropagator,
} = require("@google-cloud/opentelemetry-cloud-trace-propagator");


// Only enable OpenTelemetry if the function is actually deployed.
// Emulators don't reflect real-world latency"
if (!process.env.FUNCTIONS_EMULATOR) {
const sdk = new opentelemetry.NodeSDK({
// Setup automatic instrumentation for
// http, grpc, and express modules.
instrumentations: [
new HttpInstrumentation(),
new GrpcInstrumentation(),
new ExpressInstrumentation(),
],
// Make sure opentelemetry know about Cloud Trace http headers
// i.e. 'X-Cloud-Trace-Context'
textMapPropagator: new CloudPropagator(),
// Automatically detect and include span metadata when running
// in GCP, e.g. region of the function.
resourceDetectors: [gcpDetector],
// Export generated traces to Cloud Trace.
traceExporter: new TraceExporter(),
});

sdk.start();

// Ensure that generated traces are exported when the container is
// shutdown.
process.on("SIGTERM", async () => {
await sdk.shutdown();
});
}