CLOVER🍀

That was when it all began.

Eclipse Starter for Jakarta EEを試す

これは、なにをしたくて書いたもの?

Jakarta EEのプロジェクトのとっかかりを作れる、Eclipse Starter for Jakarta EEというものがあることは知っていたのですが、使わないまま
存在を完全に忘れていたので、1度試しておくことにしました。

Eclipse Starter for Jakarta EE

Eclipse Starter for Jakarta EEは、こちらのサイトになります。

Eclipse Starter for Jakarta EE

こんな感じで、

をそれぞれ選択していきます。

あとは「Generate」ボタンを押すと、jakartaee-hello-world.zipというzipファイルがダウンロードできます。

試してみる

今回の環境は、こちらです。

$ java --version
openjdk 17.0.8.1 2023-08-24
OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.8.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-83-generic", arch: "amd64", family: "unix"

Eclipse Starter for Jakarta EEで指定する内容は、先程のキャプチャーの通りなのですが

で作成したzipファイルをダウンロードして、展開。

$ unzip jakartaee-hello-world.zip
$ cd jakartaee-hello-world

中身はこんな感じですね。

$ tree
.
├── Dockerfile
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    └── main
        ├── java
        │   └── org
        │       └── eclipse
        │           └── jakarta
        │               └── hello
        │                   ├── Hello.java
        │                   └── HelloWorldResource.java
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            ├── images
            │   └── jakartaee_logo.jpg
            └── index.html

10 directories, 10 files

中身を少し見てみましょう。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>org.eclipse</groupId>
        <artifactId>jakartaee-hello-world</artifactId>
        <version>0.1-SNAPSHOT</version>
        <packaging>war</packaging>

        <name>jakartaee-hello-world</name>
        <description>
                This is a very simple Jakarta EE application generated by the official Eclipse Starter.
        </description>

        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <project.report.sourceEncoding>UTF-8</project.report.sourceEncoding>
                <maven.compiler.release>17</maven.compiler.release>
                <jakartaee-api.version>10.0.0</jakartaee-api.version>
                <wildfly.version>27.0.1.Final</wildfly.version>
                <compiler-plugin.version>3.11.0</compiler-plugin.version>
                <war-plugin.version>3.3.2</war-plugin.version>
                <wildfly-plugin.version>4.1.0.Final</wildfly-plugin.version>
        </properties>

        <dependencies>
                <dependency>
                        <groupId>jakarta.platform</groupId>
                        <artifactId>jakarta.jakartaee-api</artifactId>
                        <version>${jakartaee-api.version}</version>
                        <scope>provided</scope>
                </dependency>
        </dependencies>

        <build>
                <finalName>jakartaee-hello-world</finalName>
                <plugins>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <version>${compiler-plugin.version}</version>
                        </plugin>
                        <plugin>
                                <artifactId>maven-war-plugin</artifactId>
                                <version>${war-plugin.version}</version>
                                <configuration>
                                        <failOnMissingWebXml>false</failOnMissingWebXml>
                                </configuration>
                        </plugin>

                        <!-- Execute 'mvn clean package wildfly:dev' to run the application. -->
                        <plugin>
                                <groupId>org.wildfly.plugins</groupId>
                                <artifactId>wildfly-maven-plugin</artifactId>
                                <version>${wildfly-plugin.version}</version>
                                <configuration>
                                        <version>${wildfly.version}</version>
                                        <server-config>standalone-full.xml</server-config>
                                </configuration>
                        </plugin>
                </plugins>
        </build>
</project>

src/main/java/org/eclipse/jakarta/hello/HelloWorldResource.java

package org.eclipse.jakarta.hello;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

@Path("hello")
public class HelloWorldResource {

        @GET
        @Produces({ MediaType.APPLICATION_JSON })
        public Hello hello(@QueryParam("name") String name) {
                if ((name == null) || name.trim().isEmpty())  {
                        name = "world";
                }

                return new Hello(name);
        }
}

src/main/java/org/eclipse/jakarta/hello/Hello.java

package org.eclipse.jakarta.hello;

public class Hello {

        private String name;

        public Hello(String name) {
        this.name = name;
        }

        public String getHello(){
                return name;
        }
}

src/main/webapp/WEB-INF/web.xml

<web-app version="6.0"
        xmlns="https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd">
        <servlet>
                <servlet-name>jakarta.ws.rs.core.Application</servlet-name>
        </servlet>
        <servlet-mapping>
                <servlet-name>jakarta.ws.rs.core.Application</servlet-name>
                <url-pattern>/rest/*</url-pattern>
        </servlet-mapping>
        <welcome-file-list>
                <welcome-file>index.html</welcome-file>
        </welcome-file-list>
</web-app>

Dockerfile

FROM quay.io/wildfly/wildfly
COPY target/jakartaee-hello-world.war /opt/jboss/wildfly/standalone/deployments/
CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0", "-c","standalone-full.xml"]

主要なファイルはこんなところでしょうか。WildFlyのバージョンが27.0.1.Finalと微妙に古いですね。

実行方法はREADME.mdに書かれているので、まずは起動してみます。

$ mvn clean package wildfly:dev

確認。

$ curl localhost:8080/jakartaee-hello-world/rest//hello
{"hello":"world"}


$ curl localhost:8080/jakartaee-hello-world/rest//hello?name=jakarta
{"hello":"jakarta"}

OKですね。

wildfly:devで起動しているので、ソースコードを書き換えると

        @GET
        @Produces({ MediaType.APPLICATION_JSON })
        public Hello hello(@QueryParam("name") String name) {
                if ((name == null) || name.trim().isEmpty())  {
                        name = "world";
                }

                name = name + "!!";

                return new Hello(name);
        }

変更が検知されて、すぐにビルド&再デプロイされます。

[INFO] Changes detected - recompiling the module! :source
[INFO] Compiling 2 source files with javac [debug release 17] to target/classes
[INFO] Exploding webapp
[INFO] Assembling webapp [jakartaee-hello-world] in [/path/to/jakartaee-hello-world/target/jakartaee-hello-world]
[INFO] Processing war project
[INFO] Copying webapp resources [/path/to/jakartaee-hello-world/src/main/webapp]
22:54:17,068 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 23) WFLYUT0022: Unregistered web context: '/jakartaee-hello-world' from server 'default-server'
22:54:17,101 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-5) WFLYSRV0028: Stopped deployment jakartaee-hello-world (runtime-name: jakartaee-hello-world.war) in 38ms
22:54:17,105 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-8) WFLYSRV0027: Starting deployment of "jakartaee-hello-world" (runtime-name: "jakartaee-hello-world.war")
22:54:17,307 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 90) WFLYUT0021: Registered web context: '/jakartaee-hello-world' for server 'default-server'
22:54:17,324 INFO  [org.jboss.as.server] (management-handler-thread - 1) WFLYSRV0013: Redeployed "jakartaee-hello-world.war"

〜省略〜

確認。

$ curl localhost:8080/jakartaee-hello-world/rest//hello
{"hello":"world!!"}


$ curl localhost:8080/jakartaee-hello-world/rest//hello?name=jakarta
{"hello":"jakarta!!"}

まあ、これはWildFlyの話ですが。

Eclipse Starter for Jakarta EEのGitHubリポジトリ

Eclipse Starter for Jakarta EEはGitHubソースコードが公開されています。こちらですね。

GitHub - eclipse-ee4j/starter: Eclipse Starter for Jakarta EE

検索しても見つかりますし、Webサイトのissue trackerの向き先がGitHubになっていることでも気づくことができます。

README.mdを見るとわかるのですが、Web UIだけではなくてMavenアーキタイプとしても実行できます。

Eclipse Starter for Jakarta EE / Generate Jakarta EE Project Using Archetypes

MavenアーキタイプとWeb UIが含まれているリポジトリーになるので、実際にWebサイトがどのバージョンで動作しているのか
わからなくなるのですが…現時点だとMavenアーキタイプは2.2.0、2.2.0を使うWeb UIは2.0.2のようです。

https://github.com/eclipse-ee4j/starter/tree/archetype-2.2.0

https://github.com/eclipse-ee4j/starter/tree/ui-2.0.2

今回は、Mavenアーキタイプの2.2.0のタグで見ていくことにしましょう。

ディレクトリはこちら。

https://github.com/eclipse-ee4j/starter/tree/archetype-2.2.0/archetype

コマンドの例として以下が書かれていますが、どういったものが指定できるのか(指定するのか)気になりますね。

$ mvn archetype:generate -DarchetypeGroupId="org.eclipse.starter" -DarchetypeArtifactId="jakarta-starter"

設定できるプロパティは、以下に定義されています。

  <requiredProperties>
    <requiredProperty key="groupId">
      <defaultValue>org.eclipse</defaultValue>
    </requiredProperty>
    <requiredProperty key="artifactId">
      <defaultValue>jakartaee-hello-world</defaultValue>
    </requiredProperty>
    <requiredProperty key="version">
      <defaultValue>0.1-SNAPSHOT</defaultValue>
    </requiredProperty>
    <requiredProperty key="package">
      <defaultValue>org.eclipse</defaultValue>
    </requiredProperty>
    <requiredProperty key="jakartaVersion">
      <defaultValue>10</defaultValue>
      <validationRegex>^(8|9|9.1|10)$</validationRegex>
    </requiredProperty>
    <requiredProperty key="profile">
      <defaultValue>full</defaultValue>
      <validationRegex>^(core|web|full)$</validationRegex>
    </requiredProperty>
    <requiredProperty key="javaVersion">
      <defaultValue>17</defaultValue>
      <validationRegex>^(8|11|17)$</validationRegex>
    </requiredProperty>
    <requiredProperty key="docker">
      <defaultValue>no</defaultValue>
      <validationRegex>^(no|yes)$</validationRegex>
    </requiredProperty>
    <requiredProperty key="runtime">
      <defaultValue>none</defaultValue>
      <validationRegex>^(none|glassfish|open-liberty|payara|tomee|wildfly)$</validationRegex>
    </requiredProperty>
  </requiredProperties>

https://github.com/eclipse-ee4j/starter/blob/archetype-2.2.0/archetype/src/main/resources/META-INF/maven/archetype-metadata.xml

雰囲気、なんとなくわかりますね。

アプリケーションサーバーなどのバージョンがどこで決まるかというと、pom.xml内にテンプレートが埋め込まれています。

#if (${profile} == 'core')
#set ($eeArtifactId = "jakarta.jakartaee-core-api")
#set ($wildflyConfiguration = "standalone")
#elseif (${profile} == 'web')
#set ($eeArtifactId = "jakarta.jakartaee-web-api")
#set ($wildflyConfiguration = "standalone")
#else
#set ($eeArtifactId = "jakarta.jakartaee-api")
#set ($wildflyConfiguration = "standalone-full")
#end
#if (${jakartaVersion} == '10')
#set ($eeApiVersion = "10.0.0")
#elseif (${jakartaVersion} == '9.1')
#set ($eeApiVersion = "9.1.0")
#elseif (${jakartaVersion} == '9')
#set ($eeApiVersion = "9.0.0")
#else
#set ($eeApiVersion = "8.0.0")
#end
#if (${jakartaVersion} == '8')
#set ($tomeeVersion = "8.0.14")
#set ($payaraVersion = "5.2022.5")
#set ($glassfishContainerId = "glassfish5x")
#if (${profile} == 'web')
#set ($glassfishUrl = "https://repo.maven.apache.org/maven2/org/glassfish/main/distributions/web/5.1.0/web-5.1.0.zip")
#else
#set ($glassfishUrl = "https://repo.maven.apache.org/maven2/org/glassfish/main/distributions/glassfish/5.1.0/glassfish-5.1.0.zip")
#end
#else
#set ($payaraVersion = "6.2023.5")
#set ($tomeeVersion = "9.0.0")
#set ($glassfishContainerId = "glassfish7x")
#if (${profile} == 'web')
#set ($glassfishUrl = "https://repo.maven.apache.org/maven2/org/glassfish/main/distributions/web/7.0.5/web-7.0.5.zip")
#else
#set ($glassfishUrl = "https://repo.maven.apache.org/maven2/org/glassfish/main/distributions/glassfish/7.0.5/glassfish-7.0.5.zip")
#end
#end
#if (${jakartaVersion} == '10')
#set ($wildflyVersion = "27.0.1.Final")
#else
#set ($wildflyVersion = "26.1.3.Final")
#end

https://github.com/eclipse-ee4j/starter/blob/archetype-2.2.0/archetype/src/main/resources/archetype-resources/pom.xml

試しに指定すると、こんな感じでしょうか。

$ mvn archetype:generate \
  -DarchetypeGroupId="org.eclipse.starter" \
  -DarchetypeArtifactId="jakarta-starter" \
  -DgroupId=org.example \
  -DartifactId=my-jakartaee-hello-world \
  -Dversion=0.0.1-SNAPSHOT \
  -Dpackage=org.example \
  -DjakartaVersion=10 \
  -Dprofile=web \
  -DjavaVersion=17 \
  -Ddocker=yes \
  -Druntime=wildfly \
  -DinteractiveMode=false

指定した項目が反映されていることが確認できます。

[INFO] Parameter: groupId, Value: org.example
[INFO] Parameter: artifactId, Value: my-jakartaee-hello-world
[INFO] Parameter: version, Value: 0.0.1-SNAPSHOT
[INFO] Parameter: package, Value: org.example
[INFO] Parameter: packageInPathFormat, Value: org/example
[INFO] Parameter: package, Value: org.example
[INFO] Parameter: javaVersion, Value: 17
[INFO] Parameter: groupId, Value: org.example
[INFO] Parameter: profile, Value: web
[INFO] Parameter: runtime, Value: wildfly
[INFO] Parameter: artifactId, Value: my-jakartaee-hello-world
[INFO] Parameter: version, Value: 0.0.1-SNAPSHOT
[INFO] Parameter: jakartaVersion, Value: 10
[INFO] Parameter: docker, Value: yes

できあがったプロジェクト内に移動。

$ cd my-jakartaee-hello-world

確認。

$ tree
.
├── Dockerfile
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    └── main
        ├── java
        │   └── org
        │       └── example
        │           └── jakarta
        │               └── hello
        │                   ├── Hello.java
        │                   └── HelloWorldResource.java
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            ├── images
            │   └── jakartaee_logo.jpg
            └── index.html

10 directories, 10 files

pom.xml

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>org.example</groupId>
        <artifactId>my-jakartaee-hello-world</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>war</packaging>

        <name>jakartaee-hello-world</name>
        <description>
                This is a very simple Jakarta EE application generated by the official Eclipse Starter.
        </description>

        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <project.report.sourceEncoding>UTF-8</project.report.sourceEncoding>
                <maven.compiler.release>17</maven.compiler.release>
                <jakartaee-api.version>10.0.0</jakartaee-api.version>
                <wildfly.version>27.0.1.Final</wildfly.version>
                <compiler-plugin.version>3.11.0</compiler-plugin.version>
                <war-plugin.version>3.3.2</war-plugin.version>
                <wildfly-plugin.version>4.1.0.Final</wildfly-plugin.version>
        </properties>

        <dependencies>
                <dependency>
                        <groupId>jakarta.platform</groupId>
                        <artifactId>jakarta.jakartaee-web-api</artifactId>
                        <version>${jakartaee-api.version}</version>
                        <scope>provided</scope>
                </dependency>
        </dependencies>

        <build>
                <finalName>jakartaee-hello-world</finalName>
                <plugins>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <version>${compiler-plugin.version}</version>
                        </plugin>
                        <plugin>
                                <artifactId>maven-war-plugin</artifactId>
                                <version>${war-plugin.version}</version>
                                <configuration>
                                        <failOnMissingWebXml>false</failOnMissingWebXml>
                                </configuration>
                        </plugin>

                        <!-- Execute 'mvn clean package wildfly:dev' to run the application. -->
                        <plugin>
                                <groupId>org.wildfly.plugins</groupId>
                                <artifactId>wildfly-maven-plugin</artifactId>
                                <version>${wildfly-plugin.version}</version>
                                <configuration>
                                        <version>${wildfly.version}</version>
                                        <server-config>standalone.xml</server-config>
                                </configuration>
                        </plugin>
                </plugins>
        </build>
</project>

生成されるプロジェクトの元ネタは、こちらを参照しましょう。

https://github.com/eclipse-ee4j/starter/tree/archetype-2.2.0/archetype/src/main/resources/archetype-resources

こんなところでしょうか。

ちなみに、Web UIの方はJakarta EE 8で作られているみたいですね。

https://github.com/eclipse-ee4j/starter/blob/archetype-2.2.0/ui/pom.xml

OpenTelemetryのNode.jsライブラリーをauto-instrumentations-nodeを使わずに組み込む(トレースのみ)

これは、なにをしたくて書いたもの?

前にNode.jsでOpenTelemetry(トレースのみ)を試してみました。

Node.jsでOpenTelemetryのトレースを試す - CLOVER🍀

この時、OpenTelemetryをアプリケーションに組み込むためのメタパッケージとしてauto-instrumentations-nodeを使ったのですが、すべての
instrumentationライブラリーが含まれるためサイズが大きいという話があります。

気にならないかもしれませんが、仮に個々に使うとしたらどうなるのか?を今回試してみたいと思います。

OpenTelemetry JavaScript Instrumentationを自分で登録する

今回のお題で見るべきページは、こちらになります。

メタパッケージauto-instrumentations-nodeを使う時は、以下のようにしてauto-instrumentations-nodeのregisterスクリプト
Node.jsのオプションで指定します。

$ export NODE_OPTIONS='-r @opentelemetry/auto-instrumentations-node/register'

個々のinstrumentationライブラリーを指定する場合は、こんな感じのコードを作成して同じくNode.jsの-r(または--require)オプションで
指定するようです。

/*instrumentation.js*/
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");
const { ExpressInstrumentation } = require("@opentelemetry/instrumentation-express");

const sdk = new NodeSDK({
  ...
  instrumentations: [
    // Express instrumentation expects HTTP layer to be instrumented
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ]
});

お題

今回は、こんなお題で試してみたいと思います。

  • Redisを用意する
  • Expressを使ったアプリケーションを作成し、ioredisを使ってRedisへもアクセスする
  • トレースのテレメトリーデータはJaegerに収集する

instrumentationライブラリーの使い方の確認が主テーマなので、今回は簡単な構成にします。

環境

今回の環境は、こちら。

Node.js。

$ node --version
v18.17.1


$ npm --version
9.6.7

Redis。

$ bin/redis-server --version
Redis server v=7.2.1 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=81a2b5148e5873e4

Redisへは172.17.0.2でアクセスするものとし、redis-userpasswordで使えるユーザーを作成しているものとします。

Jaeger。

$ ./jaeger-all-in-one version
2023/09/17 10:50:43 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined
2023/09/17 10:50:43 application version: git-commit=2d351c3f30072cae7f5755be20e34c2697b9e3b5, git-version=v1.49.0, build-date=2023-09-07T13:13:08Z
{"gitCommit":"2d351c3f30072cae7f5755be20e34c2697b9e3b5","gitVersion":"v1.49.0","buildDate":"2023-09-07T13:13:08Z"}

Jaegerへは、172.17.0.3でアクセスするものとします。

アプリケーションを作成する

それではアプリケーションを作成します。こちらは、TypeScriptで作成しましょう。

Node.jsのプロジェクトの作成。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v18
$ npm i -D prettier
$ mkdir src

Expressとioredisのインストール。

$ npm i express
$ npm i -D @types/express
$ npm i ioredis

現時点での依存関係。

  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/node": "^18.17.17",
    "prettier": "^3.0.3",
    "typescript": "^5.2.2"
  },
  "dependencies": {
    "express": "^4.18.2",
    "ioredis": "^5.3.2"
  }

scripts

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "format": "prettier --write src"
  },

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

簡単なアプリケーションを作成。

src/app.ts

import express from 'express';
import Redis from 'ioredis';

const app = express();
app.use(express.text());

const redis = new Redis({
  host: '172.17.0.2',
  port: 6379,
  username: 'redis-user',
  password: 'password',
  db: 0,
});

app.get('/:id', async (req, res) => {
  const id = req.params['id'];
  const data = await redis.get(id);

  res.contentType('text/plain');
  res.send(data);
});

app.post('/:id', async (req, res) => {
  const id = req.params['id'];
  const body = req.body;

  await redis.set(id, body);

  res.contentType('text/plain');
  res.send(body);
});

const port = 3000;

app.listen(port, () => {
  console.log(`[${new Date().toISOString()}] server startup.`);
});

ビルドすると、distディレクトリ内に結果が出力されるので

$ npm run build

起動。

$ node dist/app.js
[2023-09-17T11:08:49.591Z] server startup.

動作確認。

$ curl -XPOST -H 'Content-Type: text/plain' localhost:3000/foo -d 'Hello World'
Hello World


$ curl -H 'Content-Type: text/plain' localhost:3000/foo
Hello World

OKですね。これでベースのアプリケーションは作成できました。

Node.jsのOpenTelemetry Instrumentationライブラリーを追加する

続いて、OpenTelemetryのinstrumentationライブラリーを追加しましょう。

auto-instrumentations-nodeに含まれているパッケージは以下に記載されています。

https://github.com/open-telemetry/opentelemetry-js-contrib/tree/auto-instrumentations-node-v0.39.2/metapackages/auto-instrumentations-node

今回は、この中から@opentelemetry/instrumentation-express、@opentelemetry/instrumentation-ioredis、@opentelemetry/instrumentation-httpを
使うことにします。

また、@opentelemetry/sdk-nodeも必要になります。

それぞれインストール。

$ npm i @opentelemetry/sdk-node
$ npm i @opentelemetry/instrumentation-express
$ npm i @opentelemetry/instrumentation-ioredis
$ npm i @opentelemetry/instrumentation-http

依存関係は、こうなりました。

  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/node": "^18.17.17",
    "prettier": "^3.0.3",
    "typescript": "^5.2.2"
  },
  "dependencies": {
    "@opentelemetry/instrumentation-express": "^0.33.1",
    "@opentelemetry/instrumentation-http": "^0.43.0",
    "@opentelemetry/instrumentation-ioredis": "^0.35.1",
    "@opentelemetry/sdk-node": "^0.43.0",
    "express": "^4.18.2",
    "ioredis": "^5.3.2"
  }

次に、アプリケーションに組み込むスクリプトを書いていきます。

OpenTelemetryのドキュメントではJavaScriptで直接書くか、TypeScriptで書いてts-nodeで実行という例になっていますが、今回は
TypeScriptで書いてJavaScriptにビルドして実行したいと思います。

以下の2つを見つつ、

こんなスクリプトを作成。

src/instrumentation.ts

import { NodeSDK } from '@opentelemetry/sdk-node';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';

const sdk = new NodeSDK({
  instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation(), new IORedisInstrumentation()],
});

sdk.start();

NodeSDKインスタンスを作成し、instrumentation等の設定を行い、最後にNodeSDK#startします。

ビルド。

$ npm run build

起動。

$ node dist/app.js
[2023-09-17T11:08:49.591Z] server startup.

環境変数を設定。

$ export OTEL_TRACES_EXPORTER=otlp
$ export OTEL_METRICS_EXPORTER=none
$ export OTEL_LOGS_EXPORTER=none
$ export OTEL_EXPORTER_OTLP_ENDPOINT=http://172.17.0.3:4318
$ export OTEL_NODE_RESOURCE_DETECTORS='env,host,os,process,container'
$ export OTEL_SERVICE_NAME=app
$ export NODE_OPTIONS='-r ./dist/instrumentation.js'

NODE_OPTIONSには、-rオプションで先程作成したスクリプトのビルド結果を指定します。

$ export NODE_OPTIONS='-r ./dist/instrumentation.js'

起動。

$ node dist/app.js
[2023-09-17T12:16:25.041Z] server startup.

また、NODE_OPTIONSを指定せずに以下のような指定でもOKです。

$ node -r ./dist/instrumentation.js dist/app.js

確認。

$ curl -XPOST -H 'Content-Type: text/plain' localhost:3000/foo -d 'Hello World'
Hello World


$ curl -H 'Content-Type: text/plain' localhost:3000/foo
Hello World

JaegerのWeb UI(http://[Jaegerが動作しているホスト]:16686/)にアクセスして、確認してみます。

検索すると、確認した時に記録されたトレースが表示されました。

OKそうですね。

というわけで、こんな感じでセットアップのスクリプトを書けばauto-instrumentations-nodeを使わずともOpenTelemetryによるトレースを
組み込むことができました。

ハマったこと

オマケ的に、ハマったことを書いてみます。

組み込み方がわからない

OpenTelemetryのNode.js instrumentationライブライーを組み込む時に、以下を参考にしたわけですが。

実は、パッケージ個々のページにも組み込み方が書かれています。

これらのページには、以下のようにNodeTracerProviderというものを使う方法が書かれていて、OpenTelemetryのドキュメントと異なるので
どちらが良いのか迷ったのですが。

const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');

const provider = new NodeTracerProvider();
provider.register();

registerInstrumentations({
  instrumentations: [
    // Express instrumentation expects HTTP layer to be instrumented
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ],
});

OpenTelemetryのドキュメントの履歴を見ると、NodeTracerProviderを使っていたものから書き直されていたので、ドキュメントの方が
良さそうですね。

Rework js library instrumentation (#2988) · open-telemetry/opentelemetry.io@6fadff8 · GitHub

しかも、NodeTracerProviderを使う方で試したらうまく動きませんでした…。

@opentelemetry/instrumentation-httpを追加するのを忘れる

トレース結果を見ているとExpressとioredisがあれば良さそうに思ったので、@opentelemetry/instrumentation-httpを追加するのをやめたら
見事に動かなくなりました…。

@opentelemetry/instrumentation-expressのドキュメントを見ると、@opentelemetry/instrumentation-httpが必要なことは書かれて
いるんですよね。

registerInstrumentations({
  instrumentations: [
    // Express instrumentation expects HTTP layer to be instrumented
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ],
});
ローカルモジュールを./で指定するのを忘れる

最初、スクリプト-r dist/instrumentation.jsのような指定をしていて

$ node -r dist/instrumentation.js dist/app.js

以下のエラーに悩まされました…。

node:internal/modules/cjs/loader:1080
  throw err;
  ^

Error: Cannot find module 'dist/instrumentation.js'
Require stack:
- internal/preload
    at Module._resolveFilename (node:internal/modules/cjs/loader:1077:15)
    at Module._load (node:internal/modules/cjs/loader:922:27)
    at internalRequire (node:internal/modules/cjs/loader:174:19)
    at Module._preloadModules (node:internal/modules/cjs/loader:1433:5)
    at loadPreloadModules (node:internal/process/pre_execution:598:5)
    at setupUserModules (node:internal/process/pre_execution:117:3)
    at prepareExecution (node:internal/process/pre_execution:108:5)
    at prepareMainThreadExecution (node:internal/process/pre_execution:37:3)
    at node:internal/main/run_main_module:10:1 {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ 'internal/preload' ]
}

Node.js v18.17.1

自分で作成したファイルを指定する時は、./を付けるんでした…。

おわりに

OpenTelemetryのNode.jsライブラリーを、auto-instrumentations-nodeを使わずに組み込んでみました。

できることはできるのですが、OpenTelemetryの各種instrumentatationライブラリーやOpenTelemetryのSDKの知識が相応に求められる感じ
みたいなので、素直にauto-instrumentations-nodeを使うのが良さそうに思います。

凝ったことは、もっと慣れてから…。