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 { 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; +}