From 692a984530abd913f83cb10cd97cf425a559b698 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Tue, 7 Jan 2025 09:28:45 -0800 Subject: [PATCH 1/4] chore: improved OpenAI + EDOT Node.js exapmle --- examples/openai/.npmrc | 1 + examples/openai/README.md | 64 ++++++++++++++++++++++++++++++ examples/openai/chat.js | 46 ++++++++++++++++++++++ examples/openai/embeddings.js | 73 +++++++++++++++++++++++++++++++++++ examples/openai/env.example | 22 +++++++++++ examples/openai/package.json | 27 +++++++++++++ 6 files changed, 233 insertions(+) create mode 100644 examples/openai/.npmrc create mode 100644 examples/openai/README.md create mode 100644 examples/openai/chat.js create mode 100644 examples/openai/embeddings.js create mode 100644 examples/openai/env.example create mode 100644 examples/openai/package.json diff --git a/examples/openai/.npmrc b/examples/openai/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/examples/openai/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/examples/openai/README.md b/examples/openai/README.md new file mode 100644 index 00000000..af792cee --- /dev/null +++ b/examples/openai/README.md @@ -0,0 +1,64 @@ +# OpenAI Zero-Code Instrumentation Examples + +This is an example of how to instrument OpenAI calls with zero code changes, +using `@elastic/opentelemetry-node` included in the Elastic Distribution of +OpenTelemetry Node.js ([EDOT Node.js][edot-node]). + +When OpenAI examples run, they export traces, metrics and logs to an OTLP +compatible endpoint. Traces and metrics include details such as the model used +and the duration of the LLM request. In the case of chat, Logs capture the +request and the generated response. The combination of these provide a +comprehensive view of the performance and behavior of your OpenAI usage. + +## Install + +First, set up a Node.js environment for the examples like this: +```bash +nvm use --lts +npm install +``` + +## Configure + +Copy [env.example](env.example) to `.env` and update its `OPENAI_API_KEY`. + +An OTLP compatible endpoint should be listening for traces, metrics and logs on +`http://localhost:4317`. If not, update `OTEL_EXPORTER_OTLP_ENDPOINT` as well. + +For example, if Elastic APM server is running locally, edit `.env` like this: +``` +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8200 +``` + +## Run + +There are two examples, and they run the same way: + +### Chat + +[chat.js](chat.js) asks the LLM a geography question and prints the response. + +Run it like this: +```bash +node --env-file .env --require @elastic/opentelemetry-node chat.js +``` + +You should see something like "Atlantic Ocean" unless your LLM hallucinates! + +### Embeddings + + +[embeddings.js](embeddings.js) creates in-memory VectorDB embeddings about +Elastic products. Then, it searches for one similar to a question. + +Run it like this: +```bash +node --env-file .env --require @elastic/opentelemetry-node embeddings.js +``` + +You should see something like "Connectors can help you connect to a database", +unless your LLM hallucinates! + +--- + +[edot-node]: https://github.com/elastic/elastic-otel-node/blob/main/packages/opentelemetry-node/README.md#install diff --git a/examples/openai/chat.js b/examples/openai/chat.js new file mode 100644 index 00000000..f8778cba --- /dev/null +++ b/examples/openai/chat.js @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const OpenAI = require('openai'); + +let chatModel = process.env.CHAT_MODEL ?? 'gpt-4o-mini'; + +async function main() { + const client = new OpenAI(); + + const messages = [ + { + role: 'user', + content: 'Answer in up to 3 words: Which ocean contains Bouvet Island?', + }, + ]; + + try { + const chatCompletion = await client.chat.completions.create({ + model: chatModel, + messages: messages, + }); + console.log(chatCompletion.choices[0].message.content); + } catch (err) { + console.log('chat err:', err); + process.exitCode = 1; + } +} + +main(); diff --git a/examples/openai/embeddings.js b/examples/openai/embeddings.js new file mode 100644 index 00000000..5d041d9b --- /dev/null +++ b/examples/openai/embeddings.js @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const OpenAI = require('openai'); +const { dot, norm } = require('mathjs'); + +let embeddingsModel = process.env.EMBEDDINGS_MODEL ?? 'text-embedding-3-small'; + +async function main() { + const client = new OpenAI(); + + const products = [ + "Search: Ingest your data, and explore Elastic's machine learning and retrieval augmented generation (RAG) capabilities.", + 'Observability: Unify your logs, metrics, traces, and profiling at scale in a single platform.', + 'Security: Protect, investigate, and respond to cyber threats with AI-driven security analytics.', + 'Elasticsearch: Distributed, RESTful search and analytics.', + 'Kibana: Visualize your data. Navigate the Stack.', + 'Beats: Collect, parse, and ship in a lightweight fashion.', + 'Connectors: Connect popular databases, file systems, collaboration tools, and more.', + 'Logstash: Ingest, transform, enrich, and output.', + ]; + + // Generate embeddings for each product. Keep them in an array instead of a vector DB. + const productEmbeddings = []; + for (const product of products) { + productEmbeddings.push(await createEmbedding(client, product)); + } + + const queryEmbedding = await createEmbedding( + client, + 'What can help me connect to a database?' + ); + + // Calculate cosine similarity between the query and document embeddings + const similarities = productEmbeddings.map(productEmbedding => { + return ( + dot(queryEmbedding, productEmbedding) / + (norm(queryEmbedding) * norm(productEmbedding)) + ); + }); + + // Get the index of the most similar document + const mostSimilarIndex = similarities.indexOf(Math.max(...similarities)); + + console.log(products[mostSimilarIndex]); +} + +async function createEmbedding(client, text) { + const response = await client.embeddings.create({ + input: [text], + model: embeddingsModel, + encoding_format: 'float', + }); + return response.data[0].embedding; +} + +main(); diff --git a/examples/openai/env.example b/examples/openai/env.example new file mode 100644 index 00000000..f307d178 --- /dev/null +++ b/examples/openai/env.example @@ -0,0 +1,22 @@ +# Update this with your real OpenAI API key +OPENAI_API_KEY=sk-YOUR_API_KEY + +# Uncomment to use Ollama instead of OpenAI +# OPENAI_BASE_URL=http://localhost:11434/v1 +# OPENAI_API_KEY=unused +# CHAT_MODEL=qwen2.5:0.5b +# EMBEDDINGS_MODEL=all-minilm:33m + +# OTEL_EXPORTER_* variables are not required. If you would like to change your +# OTLP endpoint to Elastic APM server using HTTP, uncomment the following: +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8200 +# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf + +OTEL_SERVICE_NAME=openai-example +OTEL_LOG_LEVEL=warn + +# Change to 'false' to hide prompt and completion content +OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true +# Change to affect behavior of which resources are detected. Note: these +# choices are specific to the runtime, in this case Node.js. +OTEL_NODE_RESOURCE_DETECTORS=container,env,host,os,serviceinstance,process,alibaba,aws,azure diff --git a/examples/openai/package.json b/examples/openai/package.json new file mode 100644 index 00000000..5b626d50 --- /dev/null +++ b/examples/openai/package.json @@ -0,0 +1,27 @@ +{ + "name": "openai-example", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "engines": { + "node": ">=20" + }, + "scripts": { + "chat": "node --env-file .env --require @elastic/opentelemetry-node chat.js", + "embeddings": "node --env-file .env --require @elastic/opentelemetry-node embeddings.js" + }, + "dependencies": { + "mathjs": "^14.0.1", + "openai": "^4.77.0" + }, + "_comment": "Override to avoid punycode warnings in recent versions of Node.JS", + "overrides": { + "node-fetch@2.x": { + "whatwg-url": "14.x" + } + }, + "_comment": "Add Elastic Distribution of OpenTelemetry (EDOT) Node.js", + "devDependencies": { + "@elastic/opentelemetry-node": "*" + } +} From cfef5332caf2e97ae473e9050bcf6ed80019a9ee Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Tue, 7 Jan 2025 09:51:04 -0800 Subject: [PATCH 2/4] linting (all style); move EDOT dep to 'dependencies' --- .gitignore | 1 + examples/openai/README.md | 6 +-- examples/openai/chat.js | 24 +++++------- examples/openai/embeddings.js | 74 +++++++++++++++++------------------ examples/openai/package.json | 9 ++--- 5 files changed, 54 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index dae2806f..2c078379 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env node_modules npm-debug.log* build diff --git a/examples/openai/README.md b/examples/openai/README.md index af792cee..9cb8717c 100644 --- a/examples/openai/README.md +++ b/examples/openai/README.md @@ -13,8 +13,9 @@ comprehensive view of the performance and behavior of your OpenAI usage. ## Install First, set up a Node.js environment for the examples like this: + ```bash -nvm use --lts +nvm use --lts # or similar to setup Node.js v20 or later npm install ``` @@ -23,8 +24,7 @@ npm install Copy [env.example](env.example) to `.env` and update its `OPENAI_API_KEY`. An OTLP compatible endpoint should be listening for traces, metrics and logs on -`http://localhost:4317`. If not, update `OTEL_EXPORTER_OTLP_ENDPOINT` as well. - +`http://localhost:4318`. If not, update `OTEL_EXPORTER_OTLP_ENDPOINT` as well. For example, if Elastic APM server is running locally, edit `.env` like this: ``` OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8200 diff --git a/examples/openai/chat.js b/examples/openai/chat.js index f8778cba..690daca8 100644 --- a/examples/openai/chat.js +++ b/examples/openai/chat.js @@ -22,25 +22,21 @@ const OpenAI = require('openai'); let chatModel = process.env.CHAT_MODEL ?? 'gpt-4o-mini'; async function main() { - const client = new OpenAI(); + const client = new OpenAI(); - const messages = [ - { - role: 'user', - content: 'Answer in up to 3 words: Which ocean contains Bouvet Island?', - }, - ]; + const messages = [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains Bouvet Island?', + }, + ]; - try { const chatCompletion = await client.chat.completions.create({ - model: chatModel, - messages: messages, + model: chatModel, + messages: messages, }); console.log(chatCompletion.choices[0].message.content); - } catch (err) { - console.log('chat err:', err); - process.exitCode = 1; - } } main(); diff --git a/examples/openai/embeddings.js b/examples/openai/embeddings.js index 5d041d9b..be21fa86 100644 --- a/examples/openai/embeddings.js +++ b/examples/openai/embeddings.js @@ -18,56 +18,56 @@ */ const OpenAI = require('openai'); -const { dot, norm } = require('mathjs'); +const {dot, norm} = require('mathjs'); let embeddingsModel = process.env.EMBEDDINGS_MODEL ?? 'text-embedding-3-small'; async function main() { - const client = new OpenAI(); + const client = new OpenAI(); - const products = [ - "Search: Ingest your data, and explore Elastic's machine learning and retrieval augmented generation (RAG) capabilities.", - 'Observability: Unify your logs, metrics, traces, and profiling at scale in a single platform.', - 'Security: Protect, investigate, and respond to cyber threats with AI-driven security analytics.', - 'Elasticsearch: Distributed, RESTful search and analytics.', - 'Kibana: Visualize your data. Navigate the Stack.', - 'Beats: Collect, parse, and ship in a lightweight fashion.', - 'Connectors: Connect popular databases, file systems, collaboration tools, and more.', - 'Logstash: Ingest, transform, enrich, and output.', - ]; + const products = [ + "Search: Ingest your data, and explore Elastic's machine learning and retrieval augmented generation (RAG) capabilities.", + 'Observability: Unify your logs, metrics, traces, and profiling at scale in a single platform.', + 'Security: Protect, investigate, and respond to cyber threats with AI-driven security analytics.', + 'Elasticsearch: Distributed, RESTful search and analytics.', + 'Kibana: Visualize your data. Navigate the Stack.', + 'Beats: Collect, parse, and ship in a lightweight fashion.', + 'Connectors: Connect popular databases, file systems, collaboration tools, and more.', + 'Logstash: Ingest, transform, enrich, and output.', + ]; - // Generate embeddings for each product. Keep them in an array instead of a vector DB. - const productEmbeddings = []; - for (const product of products) { - productEmbeddings.push(await createEmbedding(client, product)); - } + // Generate embeddings for each product. Keep them in an array instead of a vector DB. + const productEmbeddings = []; + for (const product of products) { + productEmbeddings.push(await createEmbedding(client, product)); + } - const queryEmbedding = await createEmbedding( - client, - 'What can help me connect to a database?' - ); - - // Calculate cosine similarity between the query and document embeddings - const similarities = productEmbeddings.map(productEmbedding => { - return ( - dot(queryEmbedding, productEmbedding) / - (norm(queryEmbedding) * norm(productEmbedding)) + const queryEmbedding = await createEmbedding( + client, + 'What can help me connect to a database?' ); - }); - // Get the index of the most similar document - const mostSimilarIndex = similarities.indexOf(Math.max(...similarities)); + // Calculate cosine similarity between the query and document embeddings + const similarities = productEmbeddings.map((productEmbedding) => { + return ( + dot(queryEmbedding, productEmbedding) / + (norm(queryEmbedding) * norm(productEmbedding)) + ); + }); + + // Get the index of the most similar document + const mostSimilarIndex = similarities.indexOf(Math.max(...similarities)); - console.log(products[mostSimilarIndex]); + console.log(products[mostSimilarIndex]); } async function createEmbedding(client, text) { - const response = await client.embeddings.create({ - input: [text], - model: embeddingsModel, - encoding_format: 'float', - }); - return response.data[0].embedding; + const response = await client.embeddings.create({ + input: [text], + model: embeddingsModel, + encoding_format: 'float', + }); + return response.data[0].embedding; } main(); diff --git a/examples/openai/package.json b/examples/openai/package.json index 5b626d50..6b438820 100644 --- a/examples/openai/package.json +++ b/examples/openai/package.json @@ -1,5 +1,5 @@ { - "name": "openai-example", + "name": "edot-node-openai-example", "version": "1.0.0", "private": true, "type": "commonjs", @@ -11,17 +11,14 @@ "embeddings": "node --env-file .env --require @elastic/opentelemetry-node embeddings.js" }, "dependencies": { + "@elastic/opentelemetry-node": "*", "mathjs": "^14.0.1", "openai": "^4.77.0" }, - "_comment": "Override to avoid punycode warnings in recent versions of Node.JS", + "// overrides comment": "Override to avoid punycode warnings in recent versions of Node.JS", "overrides": { "node-fetch@2.x": { "whatwg-url": "14.x" } - }, - "_comment": "Add Elastic Distribution of OpenTelemetry (EDOT) Node.js", - "devDependencies": { - "@elastic/opentelemetry-node": "*" } } From 36c1b14fe035ab87178ab3c971ec1f0e78b8d842 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Tue, 7 Jan 2025 09:51:40 -0800 Subject: [PATCH 3/4] drop the older openai chat example in favour of examples/openai/... --- examples/openai-chat.js | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 examples/openai-chat.js diff --git a/examples/openai-chat.js b/examples/openai-chat.js deleted file mode 100644 index 5c35287f..00000000 --- a/examples/openai-chat.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// Usage: -// OPENAI_API_KEY=sk-... \ -// node -r @elastic/opentelemetry-node openai-chat.js - -const {OpenAI} = require('openai'); - -async function main() { - const openai = new OpenAI(); - const result = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - {role: 'user', content: 'Why is the sky blue? Answer briefly.'}, - ], - }); - console.log(result.choices[0]?.message?.content); -} -main(); From 250f5573b40c4b79623e5f7f93ac4354aa029f41 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Tue, 7 Jan 2025 09:56:17 -0800 Subject: [PATCH 4/4] import usdage tweak --- examples/openai/chat.js | 2 +- examples/openai/embeddings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/openai/chat.js b/examples/openai/chat.js index 690daca8..afd45604 100644 --- a/examples/openai/chat.js +++ b/examples/openai/chat.js @@ -17,7 +17,7 @@ * under the License. */ -const OpenAI = require('openai'); +const {OpenAI} = require('openai'); let chatModel = process.env.CHAT_MODEL ?? 'gpt-4o-mini'; diff --git a/examples/openai/embeddings.js b/examples/openai/embeddings.js index be21fa86..bd1c01df 100644 --- a/examples/openai/embeddings.js +++ b/examples/openai/embeddings.js @@ -17,7 +17,7 @@ * under the License. */ -const OpenAI = require('openai'); +const {OpenAI} = require('openai'); const {dot, norm} = require('mathjs'); let embeddingsModel = process.env.EMBEDDINGS_MODEL ?? 'text-embedding-3-small';