LoginSignup
24
16

More than 5 years have passed since last update.

知らない言語を使ってみたい - ballerina 編 ( Microservice 志向な言語 )

Posted at

どこか遠くへ行きたい

第 4 弾は、ballerina の体験です。

ballerina

公式サイト : https://ballerina.io/
公式FAQ : https://ballerina.io/learn/faq/
API ドキュメント : https://ballerina.io/learn/api-docs/
リポジトリ : https://github.com/ballerina-platform/ballerina-lang
Examples 一覧 - https://ballerina.io/learn/by-example/
Guides 一覧 - https://ballerina.io/learn/by-guide/

◆ philosophy ◆

Ballerina makes it easy to write cloud native applications while maintaining reliability, scalability, observability, and security.

Ballerina is a compiled, transactional, statically and strongly typed programming language with textual and graphical syntaxes. Ballerina incorporates fundamental concepts of distributed system integration into the language and offers a type safe, concurrent environment to implement microservices with distributed transactions, reliable messaging, stream processing, and workflows.

用途 汎用
開始 2014/04
最新バージョン 0.990.0 ( 2018/10/13 )
ライセンス Apache License Version 2.0
プラットフォーム JVM
静的型付け
パッケージ管理 標準搭載
Githubスター 1548 ( 2018/12/18 )
  • Microservice 志向
    • 各種ネットワークプロトコルへの標準対応
      • gRPC
      • HTTP 2
      • WebSockets
      • Messaging
        • AMPQ
        • Kafka
      • RESTful
        • Open API ( Swagger )
    • デプロイメント
      • Artifact 生成
        • Dockerfile
        • Docker image
        • Kubernetes Resources
    • セキュリティ対策
      • Authentication 対応
        • Base Auth
        • jwt
        • OAuth (client only)
      • Sanitizing 強制アノテーション
    • サービスメッシュ機能を標準搭載
      • Circuit Breaker, Failover, Retry, Load Barancing...
      • Istio との共存も可能
    • Prometheus に標準対応
    • OpenTrace に標準対応
      • Zipkin, Jaeger
    • Elastic Stack への対応 ( 微妙 )
  • Visual Programming
  • 並列・非同期処理

2018/12/18 現在、Commit が 56,988、Closed Issue が 3,710、Open Issue が 584、Closed Pull Request が 8,497、 Open Pull Request が 40、 リリースページの熱量も半端ないbabel/babel に比肩する盛況ぶり。

インストール

・Windows 10 Pro 1803
・Docker for Windows 18.09.0
・Kubernetes ( docker-for-desktop ) v1.10.3
・kubectl v1.10.3
・heml v2.12.0

インストーラが提供されているので、ダウンロードしてインストールを行う。
https://ballerina.io/downloads/

一つ気を付ける必要があるのが、JDK 1.8 にしか対応していない事。
インストーラには JRE が含まれているので問題ないが、Zip ファイルを展開した場合は別途インストールが必要。

$ ballerina -v
# Ballerina 0.990.0

エディタ支援

とても充実している
https://ballerina.io/learn/tools-ides/

VS Code を使うのであれば、以下拡張が使える。
https://marketplace.visualstudio.com/items?itemName=ballerina.ballerina

オートコンプリートや定義ジャンプもでき、デバッガも用意されている。

また、その大きな特徴として、Diagram View があること。
2018-12-18_18h48_59.png

開発の流れ

以下より重要そうなトピックをピックアップしながら進める。
https://ballerina.io/learn/

初期化

$ ballerina init
# Ballerina project initialized

$ tree -a
# .
# ├── .ballerina
# │  └── .gitignore
# ├── .gitignore
# ├── Ballerina.toml
# └── hello_service.bal

Ballerina.toml がプロジェクト管理ファイルのようだ。

Ballerina.toml
[project]
org-name = "user"
version = "0.0.1"

詳細は以下
https://ballerina.io/learn/how-to-structure-ballerina-code/

実行・ビルド

init でできたファイルは以下である ( コメントは削除した )。

hello_service.bal
import ballerina/http;

service hello on new http:Listener(9090) {
    resource function sayHello(http:Caller caller, http:Request request) {
        http:Response response = new;
        response.setTextPayload("Hello Ballerina!");
        _ = caller -> respond(response);
    }
}

実行のみであれば、以下コマンドでできる。

$ ballerina run .\hello_service.bal
# Initiating service(s) in 'hello_service.bal'
# [ballerina/http] started HTTP/WS endpoint 0.0.0.0:9090
$ curl http://localhost:9090/hello/sayHello
# Hello Ballerina!

また、ビルドをする場合は以下。

# build
$ ballerina build

$ tree
# ├── Ballerina.toml
# ├── hello_service.bal
# └── target
#    ├── Ballerina.lock
#    └── hello_service.balx

target/hello_service.balx が生成されたバイナリとなっている。

$ ballerina run .\target\hello_service.balx
# Initiating service(s) in 'hello_service.bal'
# [ballerina/http] started HTTP/WS endpoint 0.0.0.0:9090

ダイアグラム

VS Code か IntelliJ を使えば、ダイアグラムを使った Visual Programming が可能。
vp.gif

こんな感じで色々とできる。
今の所あまり自由度が高いとは言えず、基本はコードの全体像を把握するためのツールと捉えたほうが良さそう。

詳細は以下
https://ballerina.io/learn/tools-ides/

デバッガ

VS Code と IntelliJ には、デバッガーも用意されている。
2018-12-19_12h02_34.png

アノテーション

ballerina は、様々なアノテーションを付与することで、service や endpoint, resource のカスタマイズができる。

hello_service.bal
import ballerina/http;

+ @http:ServiceConfig {
+    basePath: "/api"
+ }
service hello on new http:Listener(9090) {
+     @http:ResourceConfig {
+         methods: ["POST"],
+         path: "/say"
+     }
    resource function sayHello(http:Caller caller, http:Request request) {
        http:Response response = new;
        response.setTextPayload("Hello Ballerina!");
        _ = caller -> respond(response);
    }
}
$ ballerina run .\hello_service.bal
# Initiating service(s) in 'hello_service.bal'
# [ballerina/http] started HTTP/WS endpoint 0.0.0.0:9090
$ curl http://localhost:9090/api/say -X POST
# Hello Ballerina!

設定ファイル

ballerina/config を使えば、標準で設定ファイルが読み込める。

myconfig.toml
access_token = "qwertyuiop"
hello_service.bal
import ballerina/http;
+ import ballerina/config;

@http:ServiceConfig {
   basePath: "/api"
}
service hello on new http:Listener(9090) {
    @http:ResourceConfig {
        methods: ["POST"],
        path: "/say"
    }
    resource function sayHello(http:Caller caller, http:Request request) {
        http:Response response = new;
        response.setTextPayload(config:getAsString("access_token"));
        _ = caller -> respond(response);
    }
}
$ ballerina run --config myconfig.toml hello_service.bal
...

$ curl http://localhost:9090/api/say -X POST
# qwertyuiop

モジュール

モジュールを追加する場合は、結構簡単にできる。

自作モジュールは、こんな感じに作れる。

$ mkdir math
$ cd math
$ touch add.bal
$ tree 
# 
math\add.bal
public function addTwice(int a, int b) returns int {
  return a + a + b + b;
}
hello_service.bal
...
+ import user/math;
...

service hello on cmdListener {
...
+     resource function add(http:Caller caller, http:Request request) {
+         http:Response response = new;
+         response.setTextPayload(<string>math:addTwice(10, 30));
+         _ = caller -> respond(response);
+     }
}
$ ballerina run hello_service.bal
...

$ curl http://localhost:9090/api/add
# 80

import user/mathuser は、Ballerina.tomlproject.org-name から取られる。

詳細は以下
https://ballerina.io/learn/how-to-structure-ballerina-code/

公開モジュール

ballerina では、 Ballerina Central というモジュールを共有する公開リポジトリが提供されている。
https://central.ballerina.io/

公開されているモジュールを利用するには、ballerina pull で取得する必要がある。
取得されたモジュールは、~/.ballerina/cache 以下に展開される。

$ ballerina search github
# Ballerina Central
# =================
# 
# |NAME            | DESCRIPTION                   | DATE           | VERSION |
# |----------------| ------------------------------| ---------------| --------|
# |wso2/github4    | Connects to the GitHub Grap...| 2018-12-08-土   | 0.9.19  |
# |shan1024/chalk  | Bring colors to Ballerina p...| 2018-09-29-土   | 0.1.6   |
# |mirage/os       | Run OS system commands.       | 2018-08-10-金   | 0.1.0   |
# |chanakal/comm...| Generates WSO2 Committer Re...| 2018-12-09-日   | 0.9.12  |
# |maryamzi/github | A basic webhook accepting G...| 2018-08-06-月   | 0.0.3   |
# |pasanw/wordpr...| Connects to Wordpress from ...| 2018-09-28-金   | 0.0.6   |
# |anuruddha/kub...| Ballerina connector for Kub...| 2018-08-10-金   | 0.0.2   |
# |wso2/ethereum   | Connects to Ethereum from B...| 2018-12-10-月   | 0.8.5   |
# |hemikak/strin...| String utility functions fo...| 2018-08-01-水   | 0.0.4   |

$ ballerina pull wso2/github4
# wso2/github4:0.9.19 [central.ballerina.io -> home repo]

あとは、import すれば利用できる。

hello_service.bal
import ballerina/http;
import ballerina/config;
+ import wso2/github4;
...

自分が利用しているモジュール一覧は、ballerina list で確認可能

$ ballerina list
# Compiling source
#     hello_service.bal
# hello_service.bal
# ├── ballerina/http
# │   ├── ballerina/internal
# │   │   ├── ballerina/time
# │   │   └── ballerina/log
# │   ├── ballerina/system
# │   ├── ballerina/config
# │   │   └── ballerina/system
# │   ├── ballerina/math
# │   ├── ballerina/crypto
# │   ├── ballerina/mime
# │   │   ├── ballerina/io
# │   │   └── ballerina/file
# │   ├── ballerina/file
# │   ├── ballerina/time
# │   ├── ballerina/io
# │   ├── ballerina/runtime
# │   ├── ballerina/cache
# │   │   ├── ballerina/time
# │   │   ├── ballerina/task
# │   │   └── ballerina/system
# │   ├── ballerina/log
# │   ├── ballerina/reflect
# │   └── ballerina/auth
# │       ├── ballerina/system
# │       ├── ballerina/time
# │       ├── ballerina/log
# │       ├── ballerina/internal
# │       │   ├── ballerina/time
# │       │   └── ballerina/log
# │       ├── ballerina/cache
# │       │   ├── ballerina/time
# │       │   ├── ballerina/task
# │       │   └── ballerina/system
# │       ├── ballerina/runtime
# │       └── ballerina/config
# │           └── ballerina/system
# └── ballerina/config
#     └── ballerina/system

また、自分の作ったモジュールをアップロードするのも簡単。

$ ballerina push math

詳細は以下
https://ballerina.io/learn/how-to-publish-modules/

テスト

テストも標準で提供されている。Testerina と呼ばれる。
先程の math モジュールにテストを追加する。


```powershell
$ mkdir .\math\tests
$ touch .\math\tests\addTest.bal
math\tests\addTest.bal
import ballerina/io;
import ballerina/test;

@test:BeforeSuite
function beforeFunc() {
    io:println("called once before all tests");
}

@test:BeforeEach
function beforeEachFunc() {
    io:println("called before every test");
}

@test:Config
function testFunction() {
    test:assertEquals(addTwice(10, 40), 100 , msg = "Failed");
}

@test:AfterSuite
function afterFunc() {
    io:println("called once after all tests");
}
$ ballerina test math
# Compiling tests
#     user/math:0.0.1
# 
# Running tests
#     user/math:0.0.1
# called once before all tests
# called before every test
# called once after all tests
#         [pass] testFunction
# 
#         1 passing
#         0 failing
#         0 skipped

ソースファイル内に直接記述もできるので、そっちのほうが便利かも。

math\add.bal
import ballerina/test;

public function addTwice(int a, int b) returns int {
  return a + a + b + b;
}

@test:Config
function testFunction() {
    test:assertEquals(addTwice(10, 40), 100 , msg = "Failed");
}

詳細は以下
https://ballerina.io/learn/how-to-test-ballerina-code/

ドキュメント

コードのドキュメント記述には、Docerina という拡張 Markdown で記述する。

math\add.bal
# Add given params twice
#
#
#
# + a - One number
# + b - Other number
# + return - Added number
#
public function addTwice(int a, int b) returns int {
  return a + a + b + b;
}

2018-12-19_11h14_41.png

また、HTML でドキュメントを出力も可能です。

$ ballerina doc
# docerina: API documentation generation for sources - [math]
# docerina: HTML file written: C:\sample_1219\target\api-docs\math.html
# docerina: HTML file written: C:\sample_1219\target\api-docs\index.html
# docerina: HTML file written: C:\sample_1219\target\api-docs\module-list.html
$ cd .\target\api-doc\
$ armor

モジュールフォルダに Module.md を置いておくと、それも読み込んでくれる。
上記画像でいくと、マスモジュールです。 の所がそれだ。

詳細は以下
https://ballerina.io/learn/how-to-document-ballerina-code/

Docker

Docker に対するアノテーションを追加することで、ビルド時に Dockerfile を自動生成し、 Docker build してくれる。

hello_service.bal
import ballerina/http;
import ballerina/config;
import ballerinax/docker;

+ @docker:Config {
+     name:"helloworld",
+     tag:"v1.0"
+ }
+ @docker:CopyFiles {
+     files:[
+         {source:"./myconfig.toml", target:"/home/ballerina/conf/myconfig.toml", isBallerinaConf:true}
+     ]
+ }
+ @docker:Expose {}
+ listener http:Listener cmdListener = new(9090);


@http:ServiceConfig {
   basePath: "/api"
}
- service hello on new http:Listener(9090) 
+ service hello on cmdListener {
    @http:ResourceConfig {
        methods: ["POST"],
        path: "/say"
    }
    resource function sayHello(http:Caller caller, http:Request request) {
        http:Response response = new;
        response.setTextPayload(config:getAsString("access_token"));
        _ = caller -> respond(response);
    }
}
$ ballerina build hello_service.bal
# Compiling source
#     hello_service.bal
# Generating executable
#     ./target/hello_service.balx
#         @docker                  - complete 3/3
# 
#         Run the following command to start a Docker container:
#         docker run -d -p 9090:9090 helloworld:v1.0

$ docker images
# REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
# helloworld                                 v1.0                4bfa8d7b4f54        38 seconds ago      128MB
# ballerina/ballerina-runtime                0.990.0             5d1249acb988        8 days ago          128MB
# ...

$ docker run -d -p 9090:9090 helloworld:v1.0
# 02d446fc149b9cdf8e3d5327ac24be46affc4fa6a2f475f2f8122e72669ec39e

$ docker ps
# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
# 02d446fc149b        helloworld:v1.0     "/bin/sh -c 'balleri…"   6 seconds ago       Up 3 seconds        0.0.0.0:9090->9090/tcp   hardcor e_burnell

$ curl http://localhost:9090/api/say -X POST
# qwertyuiop

生成物を確認すると、

$ tree target
# target
# ├── Ballerina.lock
# ├── hello_service
# │  ├── Dockerfile
# │  └── myconfig.toml
# └── hello_service.balx
.\target\hello_service\Dockerfile
# Auto Generated Dockerfile

FROM ballerina/ballerina-runtime:0.990.0
LABEL maintainer="dev@ballerina.io"

COPY hello_service.balx /home/ballerina 

COPY myconfig.toml /home/ballerina/conf/myconfig.toml
EXPOSE  9090
CMD ballerina run  --config /home/ballerina/conf/myconfig.toml hello_service.balx

ベースイメージが ballerina/ballerina-runtime になっている。

Docker for Windows を利用している場合は、以下の設定をしないと ballerina から繋げない。

こんなエラーが出たら、DNS Server を固定すると解決できる。
2018-12-19_07h28_58.png

デザインプリンシプル

ここまでで、おおよその開発フローは理解できたので、ここからはもう少しコンセプトについて深めていく。

Microservice

ballerina は Microservice 志向であり、多数のサービス間でネットワーク通信をしながら協調して動作するように設計されている。

● Protocol

以下機能がデフォルトで提供されているネットワークプロトコルだ。

  • HTTP, HTTPS
    • HTTP 1.1
    • HTTP 2.0
    • 一部認証へも対応
      • Basic 認証
      • JWT
      • OAuth (クライアントのみ)
      • LDAP ?
  • WebSocket
  • WebSub
  • Database
  • gRPC
  • AMPQ (JMS)

● Traffic Management

また、サービスメッシュを構築するための機能も提供されている。

  • クライアント
    • Circuit Breaker
    • Load Balancing
    • Timeout, Retry, Failover
    • Transaction ( Experimental )

Istio が担う範囲までアプリケーションでやるつもりなんだろうか。

https://ballerina.io/learn/faq/#how-does-ballerina-compare-to-a-service-mesh-like-istio

How does Ballerina compare to a Service Mesh like Istio?

Service meshes exist to make it easier to write resilient distributed systems. They apply transaction resilience at the network request level and Ballerina applies it within the logic level. Ballerina works both with and without a service mesh! In situations where a service mesh is not present, Ballerina provides network bridging and transaction management for invocations in between services written with Ballerina or integrated via the Ballerina Bridge. In situations where a service mesh already exists, Ballerina services can be configured to delegate routing and transaction capabilities to the underlying mesh.

ということなので、Istio とは共存していくんだろうと。

Security

ネットワークには付き物のセキュリティについても配慮がなされている。
https://ballerina.io/learn/how-to-write-secure-ballerina-code/

● untrusted data

データを外部から受け取って処理をする以上、Injection や Path Manipulation の危険性からは逃れられない。
ballerina では、データの信頼性を表すアノテーションをデータに付与し、危険な操作をコンパイル時に弾くことができる。

@sensitive

Database に SQL を発行する select 関数は、引数の sqlQuery@sensitive アノテーションが付与されている。

public extern function select(@sensitive string sqlQuery, typedesc? recordType,
                              boolean loadToMemory = false, Param... parameters)
                             returns @tainted table|error;

これにより、sqlQuery には信頼できないデータは入れられない様になっている。
では、信頼できないデータとは何か。

@tainted

信頼できないデータには、@tainted アノテーションが付与される。
先程の select メソッドの戻り値が @tainted になっている。これは、SQL で取得したデータが安全ではないと考えれば当然。

この様に、外部から受け取ったデータは基本的に全て信頼できないデータとなる。

@untainted

しかし、外部から受け取ったデータは全て @sensitive に使えないと困るので、その場合はサニタイズを行い怪しさを洗浄する事でまた信頼を取り戻すことができる。

function sanitizeSortColumn (string columnName) returns @untainted string {
   string sanitizedSortColumn = columnName;
   // Sanitize logic to make sure return value is safe
   return sanitizedSortColumn;
}

この関数が返した @untainted な文字列は、@sensitive な引数へも渡すことができるようになる。
しかし、サニタイズ処理はユーザに任されているのはどうなんだろう。標準パッケージで一通りのサニタイザが提供されてるんだろうか。

● Secrets

接続情報や Token, API Key のような Secrets を使う場面は多い。
その時は、暗号化して設定ファイルに持たせることができる。

$ ballerina encrypt
# Enter value:
# 
# Enter secret:
# 
# Re-enter secret to verify:
# 
# Add the following to the runtime config:
# @encrypted:{ufeieFEIUFE309f83kJIFJjeifeKFE=}
# 
# Or add to the runtime command line:
# -e<param>=@encrypted:{ufeieFEIUFE309f83kJIFJjeifeKFE=}# 

これを設定ファイルの値にそのまま入れる。

myconfig.toml
access_token="@encrypted:{eDLMHga9NW15siGHplUzlPP+9kALRMDA1f1rPBEJXJc=}"
$ ballerina run -c .\myconfig.toml .\hello_service.bal
# ballerina: enter secret for config value decryption:
# 
# ↑ ここで、`Enter secret:` で入力したパスワードを入力
# ↓ 成功すると、サーバが動き出す
# 
# Initiating service(s) in 'hello_service.bal'
# [ballerina/http] started HTTP/WS endpoint 0.0.0.0:9090
$ curl http://localhost:9090/api/say -X POST
# encrypted value

Parallel, Asynchronous

● Parallel

ballerina は Worker と呼ばれる単位で並列化されている。ダイアグラムの Actor に相当するのがこの Worker だ。

https://ballerina.io/learn/by-example/workers.html
https://ballerina.io/learn/by-example/worker-interaction.html

import ballerina/io;
import ballerina/runtime;
public function main() {
    worker w1 {
        int i = 100;
        float k = 2.34;
        io:println("[w1 -> w2] i: ", i, " k: ", k);

        (i, k) -> w2;

        json j = {};
        j = <- w2;
        string jStr = j.toString();
        io:println("[w1 <- w2] j: ", jStr);
        io:println("[w1 ->> w2] i: ", i);

        () send = i ->> w2;

        io:println("[w1 ->> w2] successful!!");

        io:println("[w1 -> w3] k: ", k);
        k -> w3;
        k -> w3;
        k -> w3;

        io:println("Waiting for worker w3 to fetch messages..");
        error? flushResult = flush w3;
        io:println("[w1 -> w3] Flushed!!");
    }

    worker w2 {
        int iw;
        float kw;
        (int, float) vW1 = (0, 1.0);
        vW1 = <- w1;
        (iw, kw) = vW1;
        io:println("[w2 <- w1] iw: " + iw + " kw: " + kw);

        json jw = { "name": "Ballerina" };
        io:println("[w2 -> w1] jw: ", jw);
        jw -> w1;

        int lw;
        runtime:sleep(5);
        lw = <- w1;
        io:println("[w2 <- w1] lw: " + lw);
    }

    worker w3 {
        float mw;
        runtime:sleep(50);
        mw = <- w1;
        mw = <- w1;
        mw = <- w1;
        io:println("[w3 <- w1] mw: ", mw);
    }

    wait w1;
}

メッセージ送信の場合、<- で待ち受けて、-> でメッセージを送信。
->> は送信して受信を待つ。() send = i ->> w2; では i を w2 に送って、() が返るのを待つ = w2 が終わるのを待つ。

受信と送信では型検査がなされており、型安全となっている。
また、受信と送信の回数はチェックされていて、回数が合わないとコンパイルエラーとなる。
試しに foreach の中で送信してみたが、invalid worker send statement position, must be a top level statement in a worker と怒られた。

また、Fork-Join にネイティブに対応している。

fork {
    worker w1 {
        int i = 23;
        string s = "Foo";
        io:println("[w1] i: ", i, " s: ", s);

        (i, s) -> fork;
    }

    worker w2 {
        float f = 10.344;
        io:println("[w2] f: ", f);
        f -> fork;
    }

} join (all) (map results) {
    int iW1;
}

● Asynchronous

標準エンドポイントの多くは同期的に処理されるが、Future 化する事もできる。

future<http:Response | error> f1 = start nasdaqServiceEP -> get("/nasdaq/quote/GOOG");
io:println(" >> Invocation completed!" + " Proceed without blocking for a response.");

var response = await f1;

start で Future 化し、await で結果を待つ。

API

Microservice 間でのデータの通信や、外部サービスとの通信を安全に行うための様々な配慮がなされている。

● Typesafe payload

サービス間ペイロードのデータ構造としては JSON と XML をデフォルトで扱える。
ballerina では、Union 型を使ったパーシングを行いうことで、型安全を保っている。

type Payment {
    string name,
    string cardnumber,
    int month,
    int year,
    int cvc;
};

json payload = check request.getJsonPayload();
Payment|error p = <Payment>payload;
match p {
    Payment x => {
        io:println(x);
        res.statusCode = 200;
        // return the JSON that has been created
        res.setJsonPayload(check <json>x);
    }
    error e => {
        res.statusCode = 400 ;
        // return the error message if the JSON failed to parse
        res.setStringPayload(e.message);
    }
    _ = caller -> respond (res);
}

<> でキャストを行っているが、その際に成功したか失敗したかの Union 型が返ってくる。Result や Either の様なもの。

● gRPC

ballerina は、gRPC へもネイティブに対応している。

以下、proto\order.proto を実装するものとする。

proto\order.proto
syntax = "proto3";
package grpc_service;
import "google/protobuf/wrappers.proto";
service order {
    rpc findOrder(google.protobuf.StringValue) returns (google.protobuf.StringValue);
    rpc addOrder(orderInfo) returns (google.protobuf.StringValue);
    rpc updateOrder(orderInfo) returns (google.protobuf.StringValue);
    rpc cancelOrder(google.protobuf.StringValue) returns (google.protobuf.StringValue);
}
message orderInfo {
    string id = 1;
    string name = 2;
    string description = 3;
}

for Service

$ ballerina grpc --input proto\order.proto --output grpc_service --mode service
# Downloading proc executor.
# Download successfully completed.
# Successfully extracted library files.
# Successfully generated ballerina file.

$ tree grpc_service 
# grpc_service
# ├── order_pb.bal
# └── order_sample_service.bal

サービス側を作成するときは --mode service をつける。--output が出力先。

order_pb.bal が Stub で、order_sample_service.bal が実装例。

grpc_service\order_sample_service.bal
import ballerina/grpc;
listener grpc:Listener ep = new (9000);

service order on ep {
    resource function findOrder(grpc:Caller caller, string value) {
    }
    resource function addOrder(grpc:Caller caller, orderInfo value) {
    }
    resource function updateOrder(grpc:Caller caller, orderInfo value) {
    }
    resource function cancelOrder(grpc:Caller caller, string value) {
    }
}

例えば、以下のように実装していくことになる。

grpc_service\order_sample_service.bal
...

service order on ep {
...
    resource function findOrder(grpc:Caller caller, string value) {
        orderInfo o = {id: value, name: string `name {{value}}`, description: "no description"};
        var jsonValue = json.convert(o);
        if (jsonValue is error) {
            _ = caller->sendError(grpc:INTERNAL, <string>jsonValue.detail().message);
        } else {
            json orderDetails = jsonValue;
            string payload = orderDetails.toString();
            _ = caller->send(payload);
            _ = caller->complete();
        }
    }
...
}

for Client

$ ballerina grpc --input proto\order.proto --output grpc_client --mode client
# Successfully extracted library files.
# Successfully generated ballerina file.

$ tree grpc_client
# grpc_client
# ├── order_pb.bal
# └── order_sample_client.bal
grpc_service\order_sample_client.bal
import ballerina/grpc;
import ballerina/io;

public function main (string... args) {
    orderClient ep = new("http://localhost:9090");
    orderBlockingClient blockingEp = new("http://localhost:9090");

}

service orderMessageListener = service {
    resource function onMessage(string message) {
        io:println("Response received from server: " + message);
    }
    resource function onError(error err) {
        io:println("Error from Connector: " + err.reason() + " - " + <string>err.detail().message);
    }
    resource function onComplete() {
        io:println("Server Complete Sending Responses.");
    }
};

クライアント側は、main にあるように Stub の orderClientorderBlockingClient の Object を生成しながら実装していくことになるだろう。

● OpenAPI Specification

ballerina は、RESTful Endpoint からの OpenAPI Specification 生成、また OpenAPI Specification からのコード生成に対応している。
https://ballerina.io/learn/by-guide/open-api-based-service/

Ballerina → OpenAPI

ballerina のコードから OpenAPI Specification を生成する。
https://ballerina.io/learn/by-example/ballerina-to-swagger.html

import ballerina/http;
import ballerina/log;
listener http:Listener helloEp = new(9090);
service hello on helloEp {
    @http:ResourceConfig {
        methods: ["GET"],
        path: "/say"
    }
    resource function hi(http:Caller caller, http:Request request) {
        http:Response res = new;
        res.setPayload("Hello World!");
        var result = caller->respond(res);
        if (result is error) {
           log:printError("Error when responding", err = result);
        }
    }
}
$ ballerina swagger export .\hello_service.bal
# successfully generated swagger definition for input file - .\hello_service.bal

これで、ルートフォルダに hello_service.swagger.yaml ができる。

Swagger UI で見てみると、

$ docker run -p 8080:8080 -v "${pwd}\hello_service.swagger.yaml:/swagger.yaml" -e SWAGGER_JSON=/swagger.yaml swaggerapi/swagger-ui

OpenAPI → Ballerina

今度は、OpenAPI Specification から Ballerina の実装 ( Service Mock / Client ) のコードを生成する。
先程生成した定義を使う。

1. Service Mock

-m で出力するモジュールを指定する。-o で出力先フォルダを指定することもできる。

$ ballerina swagger mock .\hello_service.swagger.yaml -m swagger_mock
# successfully generated ballerina mock service for input file - .\hello_service.swagger.yaml

$ tree swagger_mock
# swagger_mock      
# ├── gen           
# │  ├── hello.bal  
# │  └── schema.bal 
# └── hello_impl.bal
swagger_mock\gen\hello.bal
import ballerina/http;
import ballerina/log;
import ballerina/mime;
import ballerina/swagger;

listener http:Listener ep0 = new(9090);

@swagger:ServiceInfo {
    title: "hello",
    serviceVersion: "1.0.0"
}
@http:ServiceConfig {
    basePath: "/"
}
service hello on ep0 {
    @swagger:ResourceInfo {
        summary: ""
    }
    @http:ResourceConfig {
        methods:["GET"],
        path:"/say"
    }
    resource function hi (http:Caller outboundEp, http:Request _hiReq) {
        http:Response _hiRes = hi(_hiReq);
        _ = outboundEp->respond(_hiRes);
    }
}
swagger_mock\hello_impl.bal
public function hi (http:Request _hiReq) returns http:Response {
    // stub code - fill as necessary
    http:Response _hiRes = new;
    string _hiPayload = "Sample hi Response";
    _hiRes.setTextPayload(_hiPayload);

    return _hiRes;
}

実装と API を分けるのは、gRPC と同じ考え方。

2. Client
$ ballerina swagger client .\hello_service.swagger.yaml -m swagger_client
successfully generated ballerina client for input file - .\hello_service.swagger.yaml

$ tree swagger_client
# swagger_mock      
# └── gen           
#    ├── hello.bal  
#    └── schema.bal 
swagger_mock\gen\hello.bal
import ballerina/io;
import ballerina/mime;
import ballerina/http;

//=====================================
//============Client Config============
//=====================================
public type helloClientConfig record {
    string serviceUrl;
    http:ClientEndpointConfig clientConfig;
};

//=======================================
//============Client Endpoint============
//=======================================
public type helloClientEp object {
    public http:Client client;
    public helloClientConfig config;

    public function init(helloClientConfig config) {
        http:Client httpEp = new(config.serviceUrl, config = {auth: config.clientConfig.auth, cache: config.clientConfig.cache});
        self.client = httpEp;
        self.config = config;
    }

    public function getCallerActions() returns (helloClient) {
        return new helloClient(self);
    }
};

//==============================
//============Client============
//==============================
public type helloClient object {
    public helloClientEp clientEp;

    new (clientEp) {}

    public function hi() returns http:Response | error {
        http:Client _hiEp = self.clientEp.client;
        http:Request request = new;

        // TODO: Update the request as needed
        return check _hiEp->GET("/say", request = request);
    }

};

使う時は、接続情報を渡した helloClientEp を使って helloClient を初期化みたいな感じだろう。

Deployment

既に Dockerfile 生成や については述べた が、その他に Kubernetes へのデプロイにも対応している。
https://ballerina.io/learn/how-to-deploy-and-run-ballerina-programs/

● Docker

改めてまとめると、

  • Dockerfile generation
    • copy local files
  • Docker image generation
  • Docker push support with Docker registry
  • Docker-based Ballerina debug support
  • Copy file support

等をコード内のアノテーションで指定ができ、ballerina build 時に実行してくれる。

● Kubernetes

Kubernetes も Docker と同様、コード内のアノテーションを解釈し、Build 時に実行してくれる。

  • Kubernetes deployment
  • Kubernetes service
  • Kubernetes liveness probe
  • Kubernetes ingress
  • Kubernetes horizontal pod autoscaler
  • Docker image generation
  • Docker push with remote Docker registry
  • Kubernetes secret
  • Kubernetes config map
  • Kubernetes persistent volume claim

試しに、以下で Build してみる。( 公式サンプルでは Ingress があったが、面倒なので削除した )

hello_service.bal
import ballerina/config;
import ballerina/http;
import ballerina/log;
import ballerina/mysql;
import ballerinax/kubernetes;


mysql:Client employeeDB = new ({
    host:config:getAsString("database.host"),
    port:3306,
    name:config:getAsString("database.name"),
    username:config:getAsString("database.username"),
    password:config:getAsString("database.password")
});

@kubernetes:Service {
    serviceType:"NodePort",
    name:"ballerina-guides-employee-database-service"
}
listener http:Listener helloEp = new (9090, config = {
    secureSocket:{
        keyStore:{
            path:"${ballerina.home}/bre/security/ballerinaKeystore.p12",
            password:config:getAsString("store.key.password")
        },
        trustStore:{
            path:"${ballerina.home}/bre/security/ballerinaTruststore.p12",
            password:config:getAsString("store.trust.password")
        }
    }
});


@kubernetes:ConfigMap {
    ballerinaConf:"./myconfig.toml"
}
@kubernetes:Deployment {
    image:"employee_search_service_image:v1.0",
    name:"employee_search_service",
    copyFiles:[{target:"/ballerina/runtime/bre/lib",source:"./resources/mysql-connector-java-8.0.11.jar"}]
}
service records on helloEp {
    @http:ResourceConfig {
        methods: ["GET"],
        path: "/one"
    }
    resource function getOne(http:Caller caller, http:Request request) {
        http:Response res = new;
        res.setPayload("One");
        var result = caller->respond(res);

        if (result is error) {
           log:printError("Error when responding", err = result);
        }
    }
}

設定ファイルは以下。

myconfig.toml
[database]
host = "mysql-server"
name = "EMPLOYEE_RECORDS"
username = "root"
password = "root"

[store.key]
password = "abc123"

[store.trust]
password = "xyz123"

また、今回は依存するファイルとして mysql-connector-java-8.0.11.jar も含めたいので、MySQL のサイトから取得し、.\resources 以下に配置している。

この状態で Build を行う。

$ ballerina build
# Compiling source
#     hello_service.bal
#     user/math:0.0.1
# 
# Running tests
#     user/math:0.0.1
#         [pass] testFunction
# 
#         1 passing
#         0 failing
#         0 skipped
# 
# Generating executables
#     ./target/hello_service.balx
#         @kubernetes:Service                      - complete 1/1
#         @kubernetes:Ingress                      - complete 1/1
#         @kubernetes:Secret                       - complete 1/1
#         @kubernetes:ConfigMap                    - complete 1/1
#         @kubernetes:Deployment                   - complete 1/1
#         @kubernetes:Docker                       - complete 3/3
#         @kubernetes:Helm                         - complete 1/1
# 
#         Run the following command to deploy the Kubernetes artifacts:
#         kubectl apply -f C:\sample_1219\target\kubernetes\hello_service
# 
#         Run the following command to install the application using Helm:
#         helm install --name employee-search-service C:\sample_1219\target\kubernetes\hello_service\employee-search-service

$ tree target\kubernetes
# target/kubernetes
# └── hello_service
#    ├── docker
#    │  ├── Dockerfile
#    │  └── mysql-connector-java-8.0.13.jar
#    ├── employee-search-service
#    │  ├── Chart.yaml
#    │  └── templates
#    │     └── hello_service.yaml
#    └── hello_service.yaml

kubectl で直接扱うためのリソースが target\kubernetes\hello_service\hello_service.yaml で、Helm の Local Charts が target\kubernetes\hello_service\employee-search-service となる。

Docker Image

まずは Docker Images を確認すると、指定通りにイメージが作成されている。

$ docker images
# REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
# employee_search_service_image              v1.0                8f9a7acc559d        13 minutes ago      131MB
# ...

target\kubernetes\hello_service\docker\Dockerfile の中身は Docker アノテーションの時とほぼ同じだ。

# Auto Generated Dockerfile

FROM ballerina/ballerina-runtime:0.990.0
LABEL maintainer="dev@ballerina.io"

COPY hello_service.balx /home/ballerina 

COPY mysql-connector-java-8.0.13.jar /ballerina/runtime/bre/lib
EXPOSE  9090
CMD ballerina run --config ${CONFIG_FILE} hello_service.balx

Kubernetes resources

次に、YAML ファイルを見ていく。
大体指定したまま作成されている。

---
apiVersion: "v1"
kind: "Service"
metadata:
  annotations: {}
  finalizers: []
  labels:
    app: "hello_service"
  name: "ballerina-guides-employee-database-service"
  ownerReferences: []
spec:
  externalIPs: []
  loadBalancerSourceRanges: []
  ports:
  - port: 9090
    protocol: "TCP"
    targetPort: 9090
  selector:
    app: "hello_service"
  type: "NodePort"
---
apiVersion: "extensions/v1beta1"
kind: "Ingress"
metadata:
  annotations:
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"
    kubernetes.io/ingress.class: "nginx"
  finalizers: []
  labels:
    app: "hello_service"
  name: "ballerina-guides-employee-database-service"
  ownerReferences: []
spec:
  rules:
  - host: "ballerina.guides.io"
    http:
      paths:
      - backend:
          serviceName: "ballerina-guides-employee-database-service"
          servicePort: 9090
        path: "/"
  tls:
  - hosts:
    - "ballerina.guides.io"
---
apiVersion: "v1"
kind: "Secret"
metadata:
  annotations: {}
  finalizers: []
  labels: {}
  name: "helloep-secure-socket"
  ownerReferences: []
data: ballerinaTruststore.p12: "MIMBuTc ................<省略>"
stringData: {}
---
apiVersion: "v1"
kind: "ConfigMap"
metadata:
  annotations: {}
  finalizers: []
  labels: {}
  name: "records-ballerina-conf-config-map"
  ownerReferences: []
data:
  ballerina.conf: "[database]\nhost = \"mysql-server\"\nname = \"EMPLOYEE_RECORDS\"\
    \nusername = \"root\"\npassword = \"root\"\n\n[store.key]\npassword = \"abc123\"\
    \n\n[store.trust]\npassword = \"xyz123\"\n"
---
apiVersion: "extensions/v1beta1"
kind: "Deployment"
metadata:
  annotations: {}
  finalizers: []
  labels:
    app: "hello_service"
  name: "employee-search-service"
  ownerReferences: []
spec:
  replicas: 1
  template:
    metadata:
      annotations: {}
      finalizers: []
      labels:
        app: "hello_service"
      ownerReferences: []
    spec:
      containers:
      - args: []
        command: []
        env:
        - name: "CONFIG_FILE"
          value: "/home/ballerina/conf/ballerina.conf"
        envFrom: []
        image: "employee_search_service_image:v1.0"
        imagePullPolicy: "IfNotPresent"
        name: "employee-search-service"
        ports:
        - containerPort: 9090
          protocol: "TCP"
        volumeMounts:
        - mountPath: "\\ballerina\\runtime\\bre\\security"
          name: "helloep-secure-socket-volume"
          readOnly: true
        - mountPath: "/home/ballerina/conf/"
          name: "records-ballerina-conf-config-map-volume"
          readOnly: false
      hostAliases: []
      imagePullSecrets: []
      initContainers: []
      nodeSelector: {}
      tolerations: []
      volumes:
      - name: "helloep-secure-socket-volume"
        secret:
          items: []
          secretName: "helloep-secure-socket"
      - configMap:
          items: []
          name: "records-ballerina-conf-config-map"
        name: "records-ballerina-conf-config-map-volume"

Deploy してみる。

$ helm install --name employee-search-service .\target\kubernetes\hello_service\employee-search-service

$ kubectl get pod
# NAME                                       READY     STATUS    RESTARTS   AGE
# employee-search-service-5f647b9cd6-pxmmx   0/1       Error     1          48s

エラーで動かない。原因究明中。
しかし、Kubernetes への対応がどの様なものかは大体理解できた。

Observable

ballerina は、メトリクス監視、分散トラッキング、ログ管理機能を標準で持っている。
https://ballerina.io/learn/how-to-observe-ballerina-code/

以降は、以下のサービスを監視することとする。

hello_service.bal
import ballerina/http;
import ballerina/log;

service hello on new http:Listener(9090) {

    resource function sayHello (http:Caller caller, http:Request req) {
        log:printInfo("This is a test Info log");
        log:printError("This is a test Error log");
        log:printWarn("This is a test Warn log");
        http:Response res = new;
        res.setPayload("Hello, World!");
        _ = caller -> respond(res);
    }
}

● メトリクス監視

メトリクス監視には、Prometheus を想定している。

$ ballerina run --observe .\hello_service.bal
# ballerina: started publishing tracers to Jaeger on localhost:5775
# Initiating service(s) in 'C:\PROGRA~1\BALLER~1\BALLER~1.0\bin\..\lib\balx\prometheus\reporter.balx'
# [ballerina/http] started HTTP/WS endpoint 0.0.0.0:9797
# ballerina: started Prometheus HTTP endpoint 0.0.0.0:9797
# Initiating service(s) in 'hello_service.bal'
# [ballerina/http] started HTTP/WS endpoint 0.0.0.0:9090

--observe を付けて起動すると、メトリクス監視専用のエンドポイント ( デフォルトで Port 9797 ) を追加し、/metrics でメトリクスが公開される。

また、設定ファイルで動作をカスタムすることもできる。

myconfig.toml
[b7a.observability.metrics]
enabled=true
reporter="prometheus"

[b7a.observability.metrics.prometheus]
port=9797
host="0.0.0.0"

今回のコードで公開されるメトリクスは以下である。

$ curl http://localhost:9797/metrics
# TYPE ballerina_scheduler_waiting_for_lock_worker_count_value gauge
ballerina_scheduler_waiting_for_lock_worker_count_value 0.0
# HELP ballerina_http:Caller_requests_total_value Total number of requests
# TYPE ballerina_http:Caller_requests_total_value counter
ballerina_http:Caller_requests_total_value{action="respond",http_status_code="200",} 1.0
# HELP http_inprogress_requests_value Inprogress Requests
# TYPE http_inprogress_requests_value gauge
http_inprogress_requests_value{resource="getMetrics",service="PrometheusReporter$$service$0",} 1.0
# HELP ballerina_http:Caller_response_time_seconds_value Response Time
# TYPE ballerina_http:Caller_response_time_seconds_value gauge
ballerina_http:Caller_response_time_seconds_value{action="respond",http_status_code="200",} 0.1376805
# HELP ballerina_http:Caller_response_time_seconds A Summary of ballerina_http:Caller_response_time_seconds for window of 60000
# TYPE ballerina_http:Caller_response_time_seconds summary
ballerina_http:Caller_response_time_seconds_mean{action="respond",http_status_code="200",timeWindow="60000",} 0.0
ballerina_http:Caller_response_time_seconds_max{action="respond",http_status_code="200",timeWindow="60000",} 0.0
ballerina_http:Caller_response_time_seconds_min{action="respond",http_status_code="200",timeWindow="60000",} 0.0
ballerina_http:Caller_response_time_seconds_stdDev{action="respond",http_status_code="200",timeWindow="60000",} 0.0
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="60000",quantile="0.33",} 0.0
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="60000",quantile="0.5",} 0.0
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="60000",quantile="0.66",} 0.0
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="60000",quantile="0.99",} 0.0
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="60000",quantile="0.999",} 0.0
# HELP ballerina_http:Caller_response_time_seconds A Summary of ballerina_http:Caller_response_time_seconds for window of 300000
# TYPE ballerina_http:Caller_response_time_seconds summary
ballerina_http:Caller_response_time_seconds_mean{action="respond",http_status_code="200",timeWindow="300000",} 0.13671875
ballerina_http:Caller_response_time_seconds_max{action="respond",http_status_code="200",timeWindow="300000",} 0.13671875
ballerina_http:Caller_response_time_seconds_min{action="respond",http_status_code="200",timeWindow="300000",} 0.13671875
ballerina_http:Caller_response_time_seconds_stdDev{action="respond",http_status_code="200",timeWindow="300000",} 0.0
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="300000",quantile="0.33",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="300000",quantile="0.5",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="300000",quantile="0.66",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="300000",quantile="0.99",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="300000",quantile="0.999",} 0.13671875
# HELP ballerina_http:Caller_response_time_seconds A Summary of ballerina_http:Caller_response_time_seconds for window of 900000
# TYPE ballerina_http:Caller_response_time_seconds summary
ballerina_http:Caller_response_time_seconds_mean{action="respond",http_status_code="200",timeWindow="900000",} 0.13671875
ballerina_http:Caller_response_time_seconds_max{action="respond",http_status_code="200",timeWindow="900000",} 0.13671875
ballerina_http:Caller_response_time_seconds_min{action="respond",http_status_code="200",timeWindow="900000",} 0.13671875
ballerina_http:Caller_response_time_seconds_stdDev{action="respond",http_status_code="200",timeWindow="900000",} 0.0
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="900000",quantile="0.33",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="900000",quantile="0.5",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="900000",quantile="0.66",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="900000",quantile="0.99",} 0.13671875
ballerina_http:Caller_response_time_seconds{action="respond",http_status_code="200",timeWindow="900000",quantile="0.999",} 0.13671875
# HELP ballerina_http:Caller_3XX_requests_total_value Total number of requests that resulted in HTTP 1xx informational responses
# TYPE ballerina_http:Caller_3XX_requests_total_value counter
ballerina_http:Caller_3XX_requests_total_value{action="respond",} 0.0
# HELP ballerina_http:Caller_inprogress_requests_value Inprogress Requests
# TYPE ballerina_http:Caller_inprogress_requests_value gauge
ballerina_http:Caller_inprogress_requests_value{action="respond",} 0.0
# HELP ballerina_http:Caller_2XX_requests_total_value Total number of requests that resulted in HTTP 1xx informational responses
# TYPE ballerina_http:Caller_2XX_requests_total_value counter
ballerina_http:Caller_2XX_requests_total_value{action="respond",} 1.0
# HELP http_requests_total_value Total number of requests
# TYPE http_requests_total_value counter
http_requests_total_value{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",} 1.0
# TYPE ballerina_scheduler_running_worker_count_value gauge
ballerina_scheduler_running_worker_count_value 0.0
# HELP ballerina_http:Caller_5XX_requests_total_value Total number of requests that resulted in HTTP 1xx informational responses
# TYPE ballerina_http:Caller_5XX_requests_total_value counter
ballerina_http:Caller_5XX_requests_total_value{action="respond",} 0.0
# HELP ballerina_http:Caller_1XX_requests_total_value Total number of requests that resulted in HTTP 1xx informational responses
# TYPE ballerina_http:Caller_1XX_requests_total_value counter
ballerina_http:Caller_1XX_requests_total_value{action="respond",} 0.0
# TYPE ballerina_scheduler_excepted_worker_count_value gauge
ballerina_scheduler_excepted_worker_count_value 0.0
# TYPE ballerina_scheduler_ready_worker_count_value gauge
ballerina_scheduler_ready_worker_count_value 0.0
# TYPE ballerina_scheduler_waiting_for_response_worker_count_value gauge
ballerina_scheduler_waiting_for_response_worker_count_value 0.0
# HELP startup_time_milliseconds_value Startup time in milliseconds
# TYPE startup_time_milliseconds_value gauge
startup_time_milliseconds_value 19697.0
# HELP http_response_time_seconds_value Response Time
# TYPE http_response_time_seconds_value gauge
http_response_time_seconds_value{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",} 0.4045906
# HELP http_response_time_seconds A Summary of http_response_time_seconds for window of 60000
# TYPE http_response_time_seconds summary
http_response_time_seconds_mean{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",} 0.0
http_response_time_seconds_max{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",} 0.0
http_response_time_seconds_min{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",} 0.0
http_response_time_seconds_stdDev{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",} 0.0
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",quantile="0.33",} 0.0
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",quantile="0.5",} 0.0
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",quantile="0.66",} 0.0
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",quantile="0.99",} 0.0
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="60000",quantile="0.999",} 0.0
# HELP http_response_time_seconds A Summary of http_response_time_seconds for window of 300000
# TYPE http_response_time_seconds summary
http_response_time_seconds_mean{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",} 0.404296875
http_response_time_seconds_max{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",} 0.404296875
http_response_time_seconds_min{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",} 0.404296875
http_response_time_seconds_stdDev{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",} 0.0
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",quantile="0.33",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",quantile="0.5",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",quantile="0.66",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",quantile="0.99",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="300000",quantile="0.999",} 0.404296875
# HELP http_response_time_seconds A Summary of http_response_time_seconds for window of 900000
# TYPE http_response_time_seconds summary
http_response_time_seconds_mean{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",} 0.404296875
http_response_time_seconds_max{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",} 0.404296875
http_response_time_seconds_min{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",} 0.404296875
http_response_time_seconds_stdDev{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",} 0.0
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",quantile="0.33",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",quantile="0.5",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",quantile="0.66",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",quantile="0.99",} 0.404296875
http_response_time_seconds{protocol="http",http_url="/metrics",resource="getMetrics",http_method="GET",service="PrometheusReporter$$service$0",timeWindow="900000",quantile="0.999",} 0.404296875
# TYPE ballerina_scheduler_paused_worker_count_value gauge
ballerina_scheduler_paused_worker_count_value 0.0
# HELP ballerina_http:Caller_4XX_requests_total_value Total number of requests that resulted in HTTP 1xx informational responses
# TYPE ballerina_http:Caller_4XX_requests_total_value counter
ballerina_http:Caller_4XX_requests_total_value{action="respond",} 0.0

今回は HTTP エンドポイントのみだが、多分他のプロトコルのエンドポイントを増やせばまた違ったメトリクスが取れるのかも知れない。
後は Prometheus の収集対象に追加するだけで、メトリクス監視が可能となる。

独自メトリクスを追加するのも簡単だ。

https://ballerina.io/learn/by-example/counter-metrics.html
https://ballerina.io/learn/by-example/gauge-metrics.html

● 分散トレーシング

分散トレーシングは OpenTracing に対応しているので、OpenTracing 準拠の JaegerZipkin などを利用できる。

まずは Jaeger を立ち上げる。

$ docker run -d -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp -p5778:5778 -p16686:16686 -p14268:14268 jaegertracing/all-in-one:latest

有効にするには、メトリクスと同様 --observe を付けて起動する。
接続女王法などは設定ファイルで設定する。デフォルトで対応しているのは、JaegerZipkin だ。

myconfig.toml
[b7a.observability.tracing]
enabled=true
name="jaeger"

[b7a.observability.tracing.jaeger]
reporter.hostname="localhost"
reporter.port=5775
sampler.type="const"
sampler.param=1.0
reporter.flush.interval.ms=2000
reporter.max.buffer.spans=1000

この状態で立ち上げ、Jaeger の画面を確認すれば、トレースができていることが確認できる。

2018-12-20_21h40_45.png

スパンを操作するモジュールは、以下を参考に。
https://ballerina.io/learn/by-example/tracing.html

● ログ集約

ballerina では、ログの集約 ~ 可視化 プラットフォームとして Elastic Stack ( ELK + Beats ) をサポートしている。

ただ、サポートとは、標準の Logger で stdout にログ吐き出せるよ というだけのもので、それをファイルにリダイレクトしつつ Filebeats で監視して Logstash へと送るというやり方。

正直、サポートと謳っているのだから、てっきり beats 機能を標準的に持っているか、もしくは Elasticsearch に直接送信するのでは、と期待していた。

ログ収集には標準仕様がないので、Docker や Kubernetes への対応も考えると『stdout に出しておけばいいや』になるんだろう。
そのおかげで Fluentd での代用も可能だ。

サンプル体験

サンプルが多い!!
https://ballerina.io/learn/by-example/

ガイドも多い!!!!
https://ballerina.io/learn/by-guide/

もはや力尽きているので、これらはまた次の機会に。
ガイドを見ると、面白そうな Topics がいっぱいあるので、後でまた読みたい。

感想

まず思いついた感想が、とんでもないものに手を出してしまった でした。
言語仕様もマニュアルも膨大で、そして今も肥大化し続けている。BLAME! の超構造体状態。

DevOps を極めるとこうなるのか、という感想。しかし、これを使いこなせる Fullstack なソフトウェアエンジニアはどれほど居るんだろう。

参考

あとがき

※ この記事は個人の見解であり、所属する組織を代表するものではありません。

24
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
16