From f6894054601e88e6f84bb980bd0f212c2da87c82 Mon Sep 17 00:00:00 2001
From: Nir Gazit <nirga@users.noreply.github.com>
Date: Thu, 14 Mar 2024 03:54:21 +0100
Subject: [PATCH] feat: add observability with openllmetry (#2)

---
 create-app.ts                                 | 12 ++++
 helpers/types.ts                              |  2 +
 helpers/typescript.ts                         | 63 +++++++++++++++----
 questions.ts                                  | 22 +++++++
 .../typescript/opentelemetry/index.ts         | 12 ++++
 templates/types/simple/express/index.ts       |  3 +
 .../simple/express/src/observability/init.ts  |  1 +
 templates/types/streaming/express/index.ts    |  3 +
 .../express/src/observability/index.ts        |  1 +
 .../streaming/nextjs/app/api/chat/route.ts    |  3 +
 .../nextjs/app/observability/index.ts         |  1 +
 templates/types/streaming/nextjs/package.json |  2 +-
 .../streaming/nextjs/webpack.config.o11y.mjs  | 16 +++++
 13 files changed, 129 insertions(+), 12 deletions(-)
 create mode 100644 templates/components/observability/typescript/opentelemetry/index.ts
 create mode 100644 templates/types/simple/express/src/observability/init.ts
 create mode 100644 templates/types/streaming/express/src/observability/index.ts
 create mode 100644 templates/types/streaming/nextjs/app/observability/index.ts
 create mode 100644 templates/types/streaming/nextjs/webpack.config.o11y.mjs

diff --git a/create-app.ts b/create-app.ts
index 8d6ce9c5..8cb6f287 100644
--- a/create-app.ts
+++ b/create-app.ts
@@ -43,6 +43,7 @@ export async function createApp({
   postInstallAction,
   dataSource,
   tools,
+  observability,
 }: InstallAppArgs): Promise<void> {
   const root = path.resolve(appPath);
 
@@ -90,6 +91,7 @@ export async function createApp({
     postInstallAction,
     dataSource,
     tools,
+    observability,
   };
 
   if (frontend) {
@@ -143,5 +145,15 @@ export async function createApp({
       `file://${root}/README.md`,
     )} and learn how to get started.`,
   );
+
+  if (args.observability === "opentelemetry") {
+    console.log(
+      `\n${yellow("Observability")}: Visit the ${terminalLink(
+        "documentation",
+        "https://traceloop.com/docs/openllmetry/integrations",
+      )} to set up the environment variables and start seeing execution traces.`,
+    );
+  }
+
   console.log();
 }
diff --git a/helpers/types.ts b/helpers/types.ts
index 76be9af3..0d359423 100644
--- a/helpers/types.ts
+++ b/helpers/types.ts
@@ -16,6 +16,7 @@ export type TemplateDataSource = {
   config: TemplateDataSourceConfig;
 };
 export type TemplateDataSourceType = "none" | "file" | "folder" | "web";
+export type TemplateObservability = "none" | "opentelemetry";
 // Config for both file and folder
 export type FileSourceConfig = {
   path?: string;
@@ -49,4 +50,5 @@ export interface InstallTemplateArgs {
   externalPort?: number;
   postInstallAction?: TemplatePostInstallAction;
   tools?: Tool[];
+  observability?: TemplateObservability;
 }
diff --git a/helpers/typescript.ts b/helpers/typescript.ts
index 6c1dff17..92a04be1 100644
--- a/helpers/typescript.ts
+++ b/helpers/typescript.ts
@@ -63,6 +63,7 @@ export const installTSTemplate = async ({
   vectorDb,
   postInstallAction,
   backend,
+  observability,
 }: InstallTemplateArgs & { backend: boolean }) => {
   console.log(bold(`Using ${packageManager}.`));
 
@@ -81,19 +82,47 @@ export const installTSTemplate = async ({
   });
 
   /**
-   * If next.js is not used as a backend, update next.config.js to use static site generation.
+   * If next.js is used, update its configuration if necessary
    */
-  if (framework === "nextjs" && !backend) {
-    // update next.config.json for static site generation
-    const nextConfigJsonFile = path.join(root, "next.config.json");
-    const nextConfigJson: any = JSON.parse(
-      await fs.readFile(nextConfigJsonFile, "utf8"),
+  if (framework === "nextjs") {
+    if (!backend) {
+      // update next.config.json for static site generation
+      const nextConfigJsonFile = path.join(root, "next.config.json");
+      const nextConfigJson: any = JSON.parse(
+        await fs.readFile(nextConfigJsonFile, "utf8"),
+      );
+      nextConfigJson.output = "export";
+      nextConfigJson.images = { unoptimized: true };
+      await fs.writeFile(
+        nextConfigJsonFile,
+        JSON.stringify(nextConfigJson, null, 2) + os.EOL,
+      );
+    }
+
+    const webpackConfigOtelFile = path.join(root, "webpack.config.o11y.mjs");
+    if (observability === "opentelemetry") {
+      const webpackConfigDefaultFile = path.join(root, "webpack.config.mjs");
+      await fs.rm(webpackConfigDefaultFile);
+      await fs.rename(webpackConfigOtelFile, webpackConfigDefaultFile);
+    } else {
+      await fs.rm(webpackConfigOtelFile);
+    }
+  }
+
+  if (observability && observability !== "none") {
+    const chosenObservabilityPath = path.join(
+      templatesDir,
+      "components",
+      "observability",
+      "typescript",
+      observability,
     );
-    nextConfigJson.output = "export";
-    nextConfigJson.images = { unoptimized: true };
-    await fs.writeFile(
-      nextConfigJsonFile,
-      JSON.stringify(nextConfigJson, null, 2) + os.EOL,
+    const relativeObservabilityPath = framework === "nextjs" ? "app" : "src";
+
+    await copy(
+      "**",
+      path.join(root, relativeObservabilityPath, "observability"),
+      { cwd: chosenObservabilityPath },
     );
   }
 
@@ -202,6 +231,18 @@ export const installTSTemplate = async ({
     };
   }
 
+  if (observability === "opentelemetry") {
+    packageJson.dependencies = {
+      ...packageJson.dependencies,
+      "@traceloop/node-server-sdk": "^0.5.19",
+    };
+
+    packageJson.devDependencies = {
+      ...packageJson.devDependencies,
+      "node-loader": "^2.0.0",
+    };
+  }
+
   if (!eslint) {
     // Remove packages starting with "eslint" from devDependencies
     packageJson.devDependencies = Object.fromEntries(
diff --git a/questions.ts b/questions.ts
index af5f17d6..275c2d0a 100644
--- a/questions.ts
+++ b/questions.ts
@@ -429,6 +429,28 @@ export const askQuestions = async (
     }
   }
 
+  if (program.framework === "express" || program.framework === "nextjs") {
+    if (!program.observability) {
+      if (ciInfo.isCI) {
+        program.observability = getPrefOrDefault("observability");
+      }
+    } else {
+      const { observability } = await prompts({
+        type: "select",
+        name: "observability",
+        message: "Would you like to set up observability?",
+        choices: [
+          { title: "No", value: "none" },
+          { title: "OpenTelemetry", value: "opentelemetry" },
+        ],
+        initial: 0,
+      });
+
+      program.observability = observability;
+      preferences.observability = observability;
+    }
+  }
+
   if (!program.model) {
     if (ciInfo.isCI) {
       program.model = getPrefOrDefault("model");
diff --git a/templates/components/observability/typescript/opentelemetry/index.ts b/templates/components/observability/typescript/opentelemetry/index.ts
new file mode 100644
index 00000000..7e54b5fe
--- /dev/null
+++ b/templates/components/observability/typescript/opentelemetry/index.ts
@@ -0,0 +1,12 @@
+import * as traceloop from "@traceloop/node-server-sdk";
+import * as LlamaIndex from "llamaindex";
+
+export const initObservability = () => {
+  traceloop.initialize({
+    appName: "llama-app",
+    disableBatch: true,
+    instrumentModules: {
+      llamaIndex: LlamaIndex,
+    },
+  });
+};
diff --git a/templates/types/simple/express/index.ts b/templates/types/simple/express/index.ts
index 721c4ec9..150dbf59 100644
--- a/templates/types/simple/express/index.ts
+++ b/templates/types/simple/express/index.ts
@@ -2,6 +2,7 @@
 import cors from "cors";
 import "dotenv/config";
 import express, { Express, Request, Response } from "express";
+import { initObservability } from "./src/observability";
 import chatRouter from "./src/routes/chat.route";
 
 const app: Express = express();
@@ -11,6 +12,8 @@ const env = process.env["NODE_ENV"];
 const isDevelopment = !env || env === "development";
 const prodCorsOrigin = process.env["PROD_CORS_ORIGIN"];
 
+initObservability();
+
 app.use(express.json());
 
 if (isDevelopment) {
diff --git a/templates/types/simple/express/src/observability/init.ts b/templates/types/simple/express/src/observability/init.ts
new file mode 100644
index 00000000..2e4ce2b1
--- /dev/null
+++ b/templates/types/simple/express/src/observability/init.ts
@@ -0,0 +1 @@
+export const initObservability = () => {};
diff --git a/templates/types/streaming/express/index.ts b/templates/types/streaming/express/index.ts
index 721c4ec9..150dbf59 100644
--- a/templates/types/streaming/express/index.ts
+++ b/templates/types/streaming/express/index.ts
@@ -2,6 +2,7 @@
 import cors from "cors";
 import "dotenv/config";
 import express, { Express, Request, Response } from "express";
+import { initObservability } from "./src/observability";
 import chatRouter from "./src/routes/chat.route";
 
 const app: Express = express();
@@ -11,6 +12,8 @@ const env = process.env["NODE_ENV"];
 const isDevelopment = !env || env === "development";
 const prodCorsOrigin = process.env["PROD_CORS_ORIGIN"];
 
+initObservability();
+
 app.use(express.json());
 
 if (isDevelopment) {
diff --git a/templates/types/streaming/express/src/observability/index.ts b/templates/types/streaming/express/src/observability/index.ts
new file mode 100644
index 00000000..2e4ce2b1
--- /dev/null
+++ b/templates/types/streaming/express/src/observability/index.ts
@@ -0,0 +1 @@
+export const initObservability = () => {};
diff --git a/templates/types/streaming/nextjs/app/api/chat/route.ts b/templates/types/streaming/nextjs/app/api/chat/route.ts
index ef35bf76..32b9bb16 100644
--- a/templates/types/streaming/nextjs/app/api/chat/route.ts
+++ b/templates/types/streaming/nextjs/app/api/chat/route.ts
@@ -1,9 +1,12 @@
+import { initObservability } from "@/app/observability";
 import { StreamingTextResponse } from "ai";
 import { ChatMessage, MessageContent, OpenAI } from "llamaindex";
 import { NextRequest, NextResponse } from "next/server";
 import { createChatEngine } from "./engine";
 import { LlamaIndexStream } from "./llamaindex-stream";
 
+initObservability();
+
 export const runtime = "nodejs";
 export const dynamic = "force-dynamic";
 
diff --git a/templates/types/streaming/nextjs/app/observability/index.ts b/templates/types/streaming/nextjs/app/observability/index.ts
new file mode 100644
index 00000000..2e4ce2b1
--- /dev/null
+++ b/templates/types/streaming/nextjs/app/observability/index.ts
@@ -0,0 +1 @@
+export const initObservability = () => {};
diff --git a/templates/types/streaming/nextjs/package.json b/templates/types/streaming/nextjs/package.json
index b0af0eeb..a5872f79 100644
--- a/templates/types/streaming/nextjs/package.json
+++ b/templates/types/streaming/nextjs/package.json
@@ -24,7 +24,7 @@
     "remark-code-import": "^1.2.0",
     "remark-gfm": "^3.0.1",
     "remark-math": "^5.1.1",
-    "supports-color": "^9.4.0",
+    "supports-color": "^8.1.1",
     "tailwind-merge": "^2.1.0"
   },
   "devDependencies": {
diff --git a/templates/types/streaming/nextjs/webpack.config.o11y.mjs b/templates/types/streaming/nextjs/webpack.config.o11y.mjs
new file mode 100644
index 00000000..b28a4591
--- /dev/null
+++ b/templates/types/streaming/nextjs/webpack.config.o11y.mjs
@@ -0,0 +1,16 @@
+export default function webpack(config, isServer) {
+  // See https://webpack.js.org/configuration/resolve/#resolvealias
+  config.resolve.alias = {
+    ...config.resolve.alias,
+    sharp$: false,
+    "onnxruntime-node$": false,
+  };
+  config.module.rules.push({
+    test: /\.node$/,
+    loader: "node-loader",
+  });
+  if (isServer) {
+    config.ignoreWarnings = [{ module: /opentelemetry/ }];
+  }
+  return config;
+}
-- 
GitLab