You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2025/12/19 06:50:22 UTC

(skywalking-banyandb-client-proto) branch main updated (62bfa05 -> 374f5f8)

This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb-client-proto.git


    from 62bfa05  Init project
     new 164447d  Add proto files for Banyandb API including common, database, measure, model, property, stream, and trace definitions
     new 374f5f8  Update README.md to enhance documentation on BanyanDB client proto definitions, including sync process, prerequisites, and project structure.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .asf.yml                                  |  42 ++
 .github/workflows/ci.yml                  |  87 ++++
 .github/workflows/sync-proto.yml          |  89 ++++
 .gitignore                                |  10 +
 .licenserc.yaml                           |  35 ++
 .mvn/wrapper/maven-wrapper.jar            | Bin 0 -> 62547 bytes
 .mvn/wrapper/maven-wrapper.properties     |   2 +
 HEADER                                    |  14 +
 Makefile                                  |  77 +++
 README.md                                 | 122 ++++-
 mvnw                                      |  29 ++
 pom.xml                                   | 107 +++++
 proto/banyandb/v1/banyandb-common.proto   | 182 +++++++
 proto/banyandb/v1/banyandb-database.proto | 742 ++++++++++++++++++++++++++++
 proto/banyandb/v1/banyandb-measure.proto  | 219 +++++++++
 proto/banyandb/v1/banyandb-model.proto    | 198 ++++++++
 proto/banyandb/v1/banyandb-property.proto | 112 +++++
 proto/banyandb/v1/banyandb-stream.proto   | 130 +++++
 proto/banyandb/v1/banyandb-trace.proto    | 122 +++++
 scripts/sync_proto.py                     | 769 ++++++++++++++++++++++++++++++
 20 files changed, 3082 insertions(+), 6 deletions(-)
 create mode 100644 .asf.yml
 create mode 100644 .github/workflows/ci.yml
 create mode 100644 .github/workflows/sync-proto.yml
 create mode 100644 .gitignore
 create mode 100644 .licenserc.yaml
 create mode 100644 .mvn/wrapper/maven-wrapper.jar
 create mode 100644 .mvn/wrapper/maven-wrapper.properties
 create mode 100644 HEADER
 create mode 100644 Makefile
 create mode 100755 mvnw
 create mode 100644 pom.xml
 create mode 100644 proto/banyandb/v1/banyandb-common.proto
 create mode 100644 proto/banyandb/v1/banyandb-database.proto
 create mode 100644 proto/banyandb/v1/banyandb-measure.proto
 create mode 100644 proto/banyandb/v1/banyandb-model.proto
 create mode 100644 proto/banyandb/v1/banyandb-property.proto
 create mode 100644 proto/banyandb/v1/banyandb-stream.proto
 create mode 100644 proto/banyandb/v1/banyandb-trace.proto
 create mode 100755 scripts/sync_proto.py


(skywalking-banyandb-client-proto) 02/02: Update README.md to enhance documentation on BanyanDB client proto definitions, including sync process, prerequisites, and project structure.

Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb-client-proto.git

commit 374f5f8b7d105438fb2968cc36b55da3fdad488b
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Fri Dec 19 06:49:00 2025 +0000

    Update README.md to enhance documentation on BanyanDB client proto definitions, including sync process, prerequisites, and project structure.
---
 .asf.yml                              |  42 ++
 .github/workflows/ci.yml              |  87 ++++
 .github/workflows/sync-proto.yml      |  89 ++++
 .gitignore                            |  10 +
 .licenserc.yaml                       |  35 ++
 .mvn/wrapper/maven-wrapper.jar        | Bin 0 -> 62547 bytes
 .mvn/wrapper/maven-wrapper.properties |   2 +
 HEADER                                |  14 +
 Makefile                              |  77 ++++
 README.md                             | 122 +++++-
 mvnw                                  |  29 ++
 pom.xml                               | 107 +++++
 scripts/sync_proto.py                 | 769 ++++++++++++++++++++++++++++++++++
 13 files changed, 1377 insertions(+), 6 deletions(-)

diff --git a/.asf.yml b/.asf.yml
new file mode 100644
index 0000000..ea8e4a8
--- /dev/null
+++ b/.asf.yml
@@ -0,0 +1,42 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF 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.
+#
+
+github:
+  description: BanyanDB public client Protocol Buffer / gRPC definitions
+  homepage: https://skywalking.apache.org/
+  labels:
+    - skywalking
+    - observability
+    - apm
+    - distributed-tracing
+    - metrics
+    - logging
+    - database
+    - time-series
+  enabled_merge_buttons:
+    squash:  true
+    merge:   false
+    rebase:  false
+  protected_branches:
+    main:
+      required_status_checks:
+        strict: true
+        contexts:
+          - CI
+      required_pull_request_reviews:
+        dismiss_stale_reviews: true
+        required_approving_review_count: 1
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b271909
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,87 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF 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.
+
+name: CI
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  check-license-header:
+    name: Check License Header
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: true
+      - name: Check License Header
+        uses: apache/skywalking-eyes/header@20da317d1ad158e79e24355fdc28f53370e94c8a
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+  verify-proto:
+    name: Generate and Compile Java from Proto
+    runs-on: ubuntu-latest
+    
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      
+      - name: Set up JDK 17
+        uses: actions/setup-java@v4
+        with:
+          java-version: '17'
+          distribution: 'temurin'
+          cache: maven
+      
+      - name: Cache Maven dependencies
+        uses: actions/cache@v4
+        with:
+          path: ~/.m2
+          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-m2
+      
+      - name: Make Maven wrapper executable
+        if: hashFiles('mvnw') != ''
+        run: chmod +x mvnw
+      
+      - name: Generate Java code from proto files
+        run: |
+          if [ -f mvnw ]; then
+            ./mvnw clean compile
+          else
+            mvn clean compile
+          fi
+      
+      - name: Verify compilation succeeded
+        run: |
+          if [ ! -d "target/classes" ]; then
+            echo "Error: Java classes were not generated/compiled"
+            exit 1
+          fi
+          echo "✓ Java code generated and compiled successfully"
+          echo "Generated classes location: target/classes"
+
+
+ci:
+  name: CI
+  runs-on: ubuntu-latest
+  needs: [ check-license-header, verify-proto ]
+  steps:
+    - run: echo 'Success'
diff --git a/.github/workflows/sync-proto.yml b/.github/workflows/sync-proto.yml
new file mode 100644
index 0000000..cf82c70
--- /dev/null
+++ b/.github/workflows/sync-proto.yml
@@ -0,0 +1,89 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF 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.
+
+name: Sync Proto Files
+
+on:
+  workflow_dispatch:
+    inputs:
+      branch:
+        description: 'Branch or tag to sync from'
+        required: false
+        default: 'main'
+        type: string
+
+jobs:
+  sync-proto:
+    name: Sync Proto Files from Upstream
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+      pull-requests: write
+    
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          fetch-depth: 0
+      
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.x'
+      
+      - name: Configure Git
+        run: |
+          git config --local user.email "action@github.com"
+          git config --local user.name "GitHub Action"
+      
+      - name: Sync proto files
+        id: sync
+        run: |
+          BRANCH="${{ github.event.inputs.branch || 'main' }}"
+          echo "Syncing proto files from branch: $BRANCH"
+          python3 scripts/sync_proto.py --branch "$BRANCH" --force
+          
+          # Check if there are any changes
+          if [ -n "$(git status --porcelain)" ]; then
+            echo "changes=true" >> $GITHUB_OUTPUT
+            echo "Proto files have been updated"
+            git status --short
+          else
+            echo "changes=false" >> $GITHUB_OUTPUT
+            echo "No changes detected"
+          fi
+      
+      - name: Create Pull Request
+        if: steps.sync.outputs.changes == 'true'
+        uses: peter-evans/create-pull-request@v6
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          commit-message: "chore: sync proto files from upstream"
+          title: "chore: Sync proto files from upstream"
+          body: |
+            This PR automatically syncs proto files from the Apache SkyWalking BanyanDB repository.
+            
+            **Source branch:** `${{ github.event.inputs.branch || 'main' }}`
+            
+            Please review the changes and merge if they look correct.
+          branch: sync-proto-files
+          delete-branch: true
+          labels: |
+            automated
+            proto-sync
+          draft: false
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bfe8acf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+# Generated Java files
+target/generated-sources/**/*.java
+target/
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Cursor
+.cursor/
diff --git a/.licenserc.yaml b/.licenserc.yaml
new file mode 100644
index 0000000..a545a99
--- /dev/null
+++ b/.licenserc.yaml
@@ -0,0 +1,35 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF 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.
+#
+header:
+  license:
+    spdx-id: Apache-2.0
+    copyright-owner: Apache Software Foundation
+
+  paths-ignore:
+    - '.github/PULL_REQUEST_TEMPLATE'
+    - '**/.gitignore'
+    - '.gitmodules'
+    - '.mvn'
+    - '**/*.md'
+    - '**/*.json'
+    - '**/target/**'
+    - 'LICENSE'
+    - 'NOTICE'
+
+  comment: on-failure
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..cb28b0e
Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..097e771
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/HEADER b/HEADER
new file mode 100644
index 0000000..0ed96c4
--- /dev/null
+++ b/HEADER
@@ -0,0 +1,14 @@
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF 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.
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4bfc9ec
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,77 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF 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.
+
+# BanyanDB Client Proto - Makefile
+
+# Binaries
+MVNW := ./mvnw
+
+.PHONY: all check-versions generate compile clean sync-proto sync-proto-dry-run
+
+all: compile
+
+# Check versions
+check-versions:
+	@echo "Checking installed versions..."
+	@echo "Required: JDK $(JDK_VERSION)"
+	@echo ""
+	@if command -v javac >/dev/null 2>&1; then \
+		echo "javac version:"; \
+		javac -version 2>&1 || $(JAVAC) -version 2>&1 || echo "  Not found"; \
+	else \
+		echo "javac: Not found"; \
+	fi
+
+# Generate Java code and compile using Maven
+compile:
+	@echo "Generating Java code and compiling using Maven..."
+	@if [ ! -f pom.xml ]; then \
+		echo "Error: pom.xml not found. Please ensure pom.xml exists in the project root."; \
+		exit 1; \
+	fi
+	@if [ -f $(MVNW) ]; then \
+		MVN_CMD=$(MVNW); \
+	elif command -v mvn >/dev/null 2>&1; then \
+		MVN_CMD=mvn; \
+	else \
+		echo "Error: Maven not found. Please run 'make install-mvnw' first or install Maven."; \
+		exit 1; \
+	fi; \
+	echo "Using Maven: $$MVN_CMD"; \
+	$$MVN_CMD clean compile || exit 1; \
+	echo "Java code generated and compiled successfully."
+
+# Clean generated files and Maven build artifacts
+clean:
+	@if [ -f $(MVNW) ] || command -v mvn >/dev/null 2>&1; then \
+		if [ -f $(MVNW) ]; then \
+			$(MVNW) clean 2>/dev/null || true; \
+		elif command -v mvn >/dev/null 2>&1; then \
+			mvn clean 2>/dev/null || true; \
+		fi; \
+	fi
+	@echo "Clean completed."
+
+# Sync proto files from Apache SkyWalking BanyanDB repository
+sync-proto:
+	@echo "Syncing proto files from Apache SkyWalking BanyanDB..."
+	@python3 scripts/sync_proto.py
+
+# Dry run: Preview proto file changes without writing
+sync-proto-dry-run:
+	@echo "Dry run: Previewing proto file changes..."
+	@python3 scripts/sync_proto.py --dry-run
+
diff --git a/README.md b/README.md
index b36dd3b..e446d2f 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,130 @@
-BanyanDB Client Proto Definitions
-==========
+# BanyanDB Client Proto Definitions
 
 <img src="http://skywalking.apache.org/assets/logo.svg" alt="Sky Walking logo" height="90px" align="right" />
 
-Protocol definitions in Protobuf/gRPC for BanyanDB clients 
+This repo contains the **BanyanDB public client Protocol Buffer / gRPC definitions**, plus a small toolchain to:
 
-# Code of conduct
+- **Sync** protos from the upstream BanyanDB repo (`apache/skywalking-banyandb`)
+- **Normalize and merge** upstream protos into a stable client-facing layout under `proto/banyandb/v1/`
+- **Generate + compile Java** from the synced protos to verify the sync is valid (also enforced in CI)
+
+## Upstream source of truth
+
+Protos are synced from the Apache SkyWalking BanyanDB repository:
+
+- **Repo**: `apache/skywalking-banyandb`
+- **Path**: `api/proto/banyandb/<module>/v1/*.proto`
+
+Locally, this repo maintains a consolidated, client-friendly layout:
+
+- `proto/banyandb/v1/banyandb-common.proto`
+- `proto/banyandb/v1/banyandb-database.proto`
+- `proto/banyandb/v1/banyandb-measure.proto`
+- `proto/banyandb/v1/banyandb-model.proto`
+- `proto/banyandb/v1/banyandb-property.proto`
+- `proto/banyandb/v1/banyandb-stream.proto`
+- `proto/banyandb/v1/banyandb-trace.proto`
+
+These files are produced by `scripts/sync_proto.py`, which also rewrites imports to match the merged layout.
+
+## Prerequisites
+
+- **Python**: 3.x (for `scripts/sync_proto.py`)
+- **Java**: JDK 17 (for compilation verification)
+- **Maven**: use `./mvnw` (recommended) or a system `mvn`
+
+If you use `./mvnw` and hit a permission error, run:
+
+```bash
+chmod +x mvnw
+```
+
+## Sync protos
+
+### Preview changes (dry-run)
+
+```bash
+make sync-proto-dry-run
+```
+
+Or directly (with extra options):
+
+```bash
+python3 scripts/sync_proto.py --branch main --dry-run
+```
+
+### Apply changes
+
+Interactive (asks for confirmation):
+
+```bash
+make sync-proto
+```
+
+Non-interactive (useful in automation):
+
+```bash
+python3 scripts/sync_proto.py --branch main --force
+```
+
+### Sync only specific module(s)
+
+```bash
+python3 scripts/sync_proto.py --branch main --module common --module measure --force
+```
+
+Valid modules are: `common`, `database`, `measure`, `model`, `property`, `stream`, `trace`.
+
+### Sync via GitHub Actions (opens a PR)
+
+Run the workflow “Sync Proto Files” (`.github/workflows/sync-proto.yml`) with an optional `branch` input (defaults to `main`).
+
+## Verify the sync (generate + compile)
+
+The fastest correctness check after syncing is to **generate Java from the protos and compile**:
+
+```bash
+make compile
+```
+
+This runs `mvn clean compile` (preferring `./mvnw` if present), which:
+
+- Generates Java sources from `proto/**/*.proto` into `target/generated-sources/`
+- Compiles them into `target/classes/`
+
+This is also what CI runs in “Verify Proto Files” (`.github/workflows/verify-proto.yml`).
+
+## Clean
+
+```bash
+make clean
+```
+
+## Project structure
+
+```
+.
+├── proto/
+│   └── banyandb/
+│       └── v1/                 # Consolidated (synced) proto files
+├── scripts/
+│   └── sync_proto.py           # Sync + merge tool (pulls from upstream GitHub)
+├── pom.xml                     # Java compile verification (protoc + javac via Maven)
+├── mvnw                        # Maven wrapper (preferred)
+├── Makefile                    # Convenience targets
+└── README.md
+```
+
+## Code of conduct
 This project adheres to the Contributor Covenant [code of conduct](https://www.apache.org/foundation/policies/conduct). By participating, you are expected to uphold this code.
 Please follow the [REPORTING GUIDELINES](https://www.apache.org/foundation/policies/conduct#reporting-guidelines) to report unacceptable behavior.
 
-# Contact Us
+## Contact Us
 * Mail list: **dev@skywalking.apache.org**. Mail to `dev-subscribe@skywalking.apache.org`, follow the reply to subscribe the mail list.
 * Send `Request to join SkyWalking slack` mail to the mail list(`dev@skywalking.apache.org`), we will invite you in.
 * Twitter, [ASFSkyWalking](https://twitter.com/ASFSkyWalking)
 * QQ Group: 901167865(Recommended), 392443393
 * [bilibili B站 视频](https://space.bilibili.com/390683219)
 
-# License
+## License
 [Apache 2.0 License.](LICENSE)
\ No newline at end of file
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..6b1ef4f
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,29 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF 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.
+# ----------------------------------------------------------------------------
+
+APP_HOME=$(cd "$(dirname "$0")" && pwd)
+CLASSPATH="$APP_HOME/.mvn/wrapper/maven-wrapper.jar"
+if [ -n "$JAVA_HOME" ]; then
+    JAVACMD="$JAVA_HOME/bin/java"
+else
+    JAVACMD="java"
+fi
+cd "$APP_HOME" || { echo "Failed to cd to $APP_HOME" >&2; exit 1; }
+exec "$JAVACMD" -Dmaven.multiModuleProjectDirectory="$APP_HOME" -classpath "$CLASSPATH" org.apache.maven.wrapper.MavenWrapperMain "$@"
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6d0b722
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF 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.
+  ~
+  -->
+
+<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.apache.skywalking</groupId>
+    <artifactId>banyandb-client-proto</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+
+    <name>BanyanDB Client Proto</name>
+    <description>Protocol definitions in Protobuf/gRPC for BanyanDB clients</description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <protobuf.version>4.28.3</protobuf.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <version>${protobuf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>build.buf.protoc-gen-validate</groupId>
+            <artifactId>pgv-java-stub</artifactId>
+            <version>1.0.2</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <sourceDirectory>target/generated-sources</sourceDirectory>
+        <plugins>
+            <plugin>
+                <groupId>org.xolstice.maven.plugins</groupId>
+                <artifactId>protobuf-maven-plugin</artifactId>
+                <version>0.6.1</version>
+                <configuration>
+                    <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
+                    <protoSourceRoot>${project.basedir}/proto</protoSourceRoot>
+                    <includes>
+                        <include>**/*.proto</include>
+                    </includes>
+                    <includeStdTypes>true</includeStdTypes>
+                    <includeDependencies>true</includeDependencies>
+                    <additionalProtoPathElements>
+                        <additionalProtoPathElement>${project.basedir}</additionalProtoPathElement>
+                    </additionalProtoPathElements>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.11.0</version>
+                <configuration>
+                    <source>17</source>
+                    <target>17</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>kr.motd.maven</groupId>
+                <artifactId>os-maven-plugin</artifactId>
+                <version>1.7.1</version>
+                <executions>
+                    <execution>
+                        <phase>initialize</phase>
+                        <goals>
+                            <goal>detect</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
+
diff --git a/scripts/sync_proto.py b/scripts/sync_proto.py
new file mode 100755
index 0000000..e8115cf
--- /dev/null
+++ b/scripts/sync_proto.py
@@ -0,0 +1,769 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF 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.
+#
+
+"""
+Proto File Sync Script for Apache SkyWalking BanyanDB
+
+This script syncs proto files from the Apache SkyWalking BanyanDB repository
+and intelligently merges multiple remote files into consolidated local files.
+"""
+
+import argparse
+import os
+import re
+import sys
+from typing import Dict, List, Set, Tuple
+from urllib.request import urlopen
+from urllib.error import URLError
+
+# ANSI color codes for terminal output
+class Colors:
+    RESET = '\033[0m'
+    RED = '\033[91m'
+    GREEN = '\033[92m'
+    YELLOW = '\033[93m'
+    BLUE = '\033[94m'
+    MAGENTA = '\033[95m'
+    CYAN = '\033[96m'
+    BOLD = '\033[1m'
+
+# Configuration: Module mapping
+MODULES = {
+    'common': {'files': 'all'},
+    'database': {'files': ['schema.proto', 'rpc.proto']},
+    'measure': {'files': 'all'},
+    'model': {'files': 'all'},
+    'property': {'files': ['property.proto', 'rpc.proto']},
+    'stream': {'files': 'all'},
+    'trace': {'files': 'all'}
+}
+
+# Exclusion list: Messages and RPCs to exclude from merged files
+# Organized by module. Each module can have 'messages' and 'rpcs' lists.
+EXCLUDE_LIST = {
+    'common': {
+        'messages': [],
+        'rpcs': []
+    },
+    'database': {
+        'messages': [],
+        'rpcs': []
+    },
+    'measure': {
+        'messages': ['DeleteExpiredSegmentsRequest', 'DeleteExpiredSegmentsResponse', 'InternalWriteRequest'],
+        'rpcs': ['DeleteExpiredSegments']
+    },
+    'model': {
+        'messages': [],
+        'rpcs': []
+    },
+    'property': {
+        'messages': ['InternalUpdateRequest', 'InternalDeleteRequest', 'InternalQueryResponse', 'InternalRepairRequest', 'InternalRepairResponse'],
+        'rpcs': []
+    },
+    'stream': {
+        'messages': ['DeleteExpiredSegmentsRequest', 'DeleteExpiredSegmentsResponse', 'InternalWriteRequest'],
+        'rpcs': ['DeleteExpiredSegments']
+    },
+    'trace': {
+        'messages': ['DeleteExpiredSegmentsRequest', 'DeleteExpiredSegmentsResponse', 'InternalWriteRequest'],
+        'rpcs': ['DeleteExpiredSegments']
+    }
+}
+
+# Skip patterns: Patterns and options to skip during proto parsing
+# - line_prefixes: Lines starting with these strings will be skipped
+# - line_contains: Lines containing these strings will be skipped
+# - option_blocks: Option blocks that require brace tracking (multi-line)
+# - import_contains: Import statements containing these strings will be skipped
+SKIP_PATTERNS = {
+    'line_prefixes': [
+        'option go_package',
+    ],
+    'line_contains': [
+        'option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger)',
+    ],
+    'option_blocks': [
+        'option (google.api.http)',
+    ],
+    'import_contains': [
+        'google/api/annotations.proto',
+        'protoc-gen-openapiv2/options/annotations.proto',
+    ],
+}
+
+# GitHub repository configuration
+GITHUB_REPO = "apache/skywalking-banyandb"
+GITHUB_RAW_BASE = "https://raw.githubusercontent.com"
+REMOTE_PROTO_PATH = "api/proto/banyandb"
+
+
+def fetch_directory_listing(branch: str, module: str) -> List[str]:
+    """
+    Fetch the list of proto files in a remote directory using GitHub API.
+    Falls back to a predefined list if API fails.
+    """
+    # Try GitHub API first
+    api_url = f"https://api.github.com/repos/{GITHUB_REPO}/contents/{REMOTE_PROTO_PATH}/{module}/v1?ref={branch}"
+    
+    try:
+        import json
+        with urlopen(api_url) as response:
+            data = json.loads(response.read().decode('utf-8'))
+            proto_files = [item['name'] for item in data if item['name'].endswith('.proto')]
+            return sorted(proto_files)
+    except Exception as e:
+        print(f"{Colors.YELLOW}Warning: Could not fetch directory listing via API: {e}{Colors.RESET}")
+        print(f"{Colors.YELLOW}Trying common file names...{Colors.RESET}")
+        
+        # Fallback: Try common file names
+        common_names = ['rpc.proto', 'write.proto', 'query.proto', 'schema.proto', 
+                       'topn.proto', 'model.proto', 'common.proto']
+        found_files = []
+        for filename in common_names:
+            url = f"{GITHUB_RAW_BASE}/{GITHUB_REPO}/{branch}/{REMOTE_PROTO_PATH}/{module}/v1/{filename}"
+            try:
+                with urlopen(url) as response:
+                    if response.status == 200:
+                        found_files.append(filename)
+            except:
+                pass
+        
+        if found_files:
+            return sorted(found_files)
+        else:
+            raise Exception(f"Could not determine proto files for module '{module}'")
+
+
+def fetch_proto_file(branch: str, module: str, filename: str) -> str:
+    """Fetch a single proto file from GitHub."""
+    url = f"{GITHUB_RAW_BASE}/{GITHUB_REPO}/{branch}/{REMOTE_PROTO_PATH}/{module}/v1/{filename}"
+    
+    try:
+        with urlopen(url) as response:
+            return response.read().decode('utf-8')
+    except URLError as e:
+        raise Exception(f"Failed to fetch {url}: {e}")
+
+
+def parse_proto_file(content: str) -> Dict[str, any]:
+    """
+    Parse a proto file into structured components.
+    Returns a dict with: license, syntax, java_package, package, imports, body
+    """
+    lines = content.split('\n')
+    result = {
+        'license': [],
+        'syntax': None,
+        'java_package': None,
+        'package': None,
+        'imports': [],
+        'body': []
+    }
+    
+    license_done = False
+    syntax_done = False
+    package_done = False
+    in_http_option = False
+    brace_depth = 0
+    
+    for i, line in enumerate(lines):
+        stripped = line.strip()
+        
+        # Detect license header - collect all consecutive comment lines from the start
+        if not license_done:
+            # Check if this is a comment line (// or /* */ style)
+            is_comment = (stripped.startswith('//') or 
+                         stripped.startswith('/*') or 
+                         (stripped.startswith('*') and i > 0 and '/*' in lines[i-1]))
+            
+            if is_comment:
+                result['license'].append(line)
+                continue
+            elif not stripped:
+                # Empty line after comments - license is done
+                license_done = True
+                continue
+            else:
+                # Non-comment, non-empty line - license must have ended
+                license_done = True
+                # Fall through to process this line
+        
+        # Skip empty lines after license but before syntax
+        if license_done and not syntax_done and not stripped:
+            continue
+        
+        # Parse syntax
+        if not syntax_done and stripped.startswith('syntax ='):
+            result['syntax'] = line
+            syntax_done = True
+            continue
+        
+        # Parse java_package option
+        if stripped.startswith('option java_package'):
+            if not result['java_package']:
+                result['java_package'] = line
+            continue
+        
+        # Skip lines matching line_prefixes patterns
+        if any(stripped.startswith(prefix) for prefix in SKIP_PATTERNS['line_prefixes']):
+            continue
+        
+        # Skip lines matching line_contains patterns
+        if any(pattern in stripped for pattern in SKIP_PATTERNS['line_contains']):
+            continue
+        
+        # Track and skip option blocks (multi-line options requiring brace tracking)
+        # Check if we're starting a new option block (can appear after syntax)
+        if syntax_done:
+            for option_pattern in SKIP_PATTERNS['option_blocks']:
+                if option_pattern in stripped:
+                    # Always skip the line when we detect the pattern
+                    brace_depth = stripped.count('{') - stripped.count('}')
+                    if brace_depth > 0:
+                        # Multi-line option, start tracking
+                        in_http_option = True
+                    # For single-line options, brace_depth <= 0, so we just skip and continue
+                    continue
+            
+            if in_http_option:
+                # We're inside an option block, track braces
+                brace_depth += stripped.count('{') - stripped.count('}')
+                if brace_depth <= 0:
+                    # Reached the end of the option block
+                    in_http_option = False
+                continue
+        
+        # Parse package declaration
+        if not package_done and stripped.startswith('package '):
+            result['package'] = line
+            package_done = True
+            continue
+        
+        # Parse imports (skip patterns in import_contains)
+        if stripped.startswith('import '):
+            if not any(pattern in line for pattern in SKIP_PATTERNS['import_contains']):
+                result['imports'].append(line)
+            continue
+        
+        # Everything else is body (skip if we're in an option block being tracked)
+        if syntax_done and package_done and not in_http_option:
+            result['body'].append(line)
+    
+    return result
+
+
+def transform_import_path(import_line: str) -> str:
+    """
+    Transform import paths from old format to new merged format.
+    banyandb/{module}/v1/{file}.proto -> banyandb/v1/banyandb-{module}.proto
+    
+    Example:
+      import "banyandb/common/v1/common.proto"; -> import "banyandb/v1/banyandb-common.proto";
+      import "banyandb/model/v1/query.proto"; -> import "banyandb/v1/banyandb-model.proto";
+    """
+    import re
+    
+    # Pattern to match: banyandb/{module}/v1/{file}.proto
+    # Extract the module name and replace the entire path
+    def replace_import(match):
+        module = match.group(1)
+        return f'banyandb/v1/banyandb-{module}.proto'
+    
+    # Replace the path pattern in the import statement
+    new_line = re.sub(
+        r'banyandb/([^/]+)/v1/[^"]+\.proto',
+        replace_import,
+        import_line
+    )
+    
+    return new_line
+
+
+def filter_excluded_definitions(body_lines: List[str], exclude_messages: List[str], exclude_rpcs: List[str]) -> List[str]:
+    """
+    Filter out excluded messages and RPCs from the body.
+    
+    Args:
+        body_lines: The body lines to filter
+        exclude_messages: List of message names to exclude
+        exclude_rpcs: List of RPC names to exclude
+    
+    Returns:
+        Filtered body lines with excluded definitions removed
+    """
+    if not body_lines or (not exclude_messages and not exclude_rpcs):
+        return body_lines
+    
+    result = []
+    i = 0
+    skip_until_close = False
+    brace_depth = 0
+    
+    while i < len(body_lines):
+        line = body_lines[i]
+        stripped = line.strip()
+        
+        # Check if we're currently skipping a definition
+        if skip_until_close:
+            # Track brace depth to know when the definition ends
+            brace_depth += stripped.count('{') - stripped.count('}')
+            
+            if brace_depth <= 0:
+                # Definition ended, stop skipping
+                skip_until_close = False
+                brace_depth = 0
+            
+            i += 1
+            continue
+        
+        # Check for excluded messages
+        if exclude_messages and stripped.startswith('message '):
+            # Extract message name
+            match = re.match(r'message\s+(\w+)', stripped)
+            if match:
+                message_name = match.group(1)
+                if message_name in exclude_messages:
+                    # Start skipping this message definition
+                    skip_until_close = True
+                    brace_depth = stripped.count('{') - stripped.count('}')
+                    i += 1
+                    continue
+        
+        # Check for excluded RPCs
+        if exclude_rpcs and stripped.startswith('rpc '):
+            # Extract RPC name
+            match = re.match(r'rpc\s+(\w+)', stripped)
+            if match:
+                rpc_name = match.group(1)
+                if rpc_name in exclude_rpcs:
+                    # Check if this is a single-line RPC (ends with ;) or multi-line (has {)
+                    if stripped.endswith(';'):
+                        # Single-line RPC, skip just this line
+                        i += 1
+                        continue
+                    elif '{' in stripped:
+                        # Multi-line RPC, start skipping
+                        skip_until_close = True
+                        brace_depth = stripped.count('{') - stripped.count('}')
+                        i += 1
+                        continue
+        
+        # Keep this line
+        result.append(line)
+        i += 1
+    
+    return result
+
+
+def remove_options_from_rpc_blocks(body_lines: List[str]) -> List[str]:
+    """
+    Remove option lines from inside RPC method blocks and convert to single-line RPCs if only options remain.
+    Removes lines matching patterns in SKIP_PATTERNS['option_blocks'] from within RPC definitions.
+    Converts:
+      rpc Query(QueryRequest) returns (QueryResponse) {
+        option (google.api.http) = {...};
+      }
+    to:
+      rpc Query(QueryRequest) returns (QueryResponse);
+    """
+    if not body_lines:
+        return body_lines
+    
+    result = []
+    i = 0
+    while i < len(body_lines):
+        line = body_lines[i]
+        stripped = line.strip()
+        
+        # Check if this is an RPC line ending with {
+        if 'rpc ' in stripped and stripped.endswith('{'):
+            rpc_line = line
+            j = i + 1
+            block_content = []
+            option_count = 0
+            
+            # Scan through the RPC block and collect non-option lines
+            while j < len(body_lines):
+                block_line = body_lines[j]
+                block_stripped = block_line.strip()
+                
+                # Check if we hit another RPC or service/message definition (malformed block)
+                if (block_stripped.startswith('rpc ') or 
+                    block_stripped.startswith('service ') or 
+                    block_stripped.startswith('message ')):
+                    # Malformed RPC block (no proper closing brace), convert to single-line
+                    rpc_single_line = rpc_line.rstrip().rstrip('{').rstrip()
+                    result.append(rpc_single_line + ';')
+                    # Continue from this line (don't skip it)
+                    i = j
+                    break
+                
+                # Check if we've reached a closing brace
+                if block_stripped == '}':
+                    # This could be the RPC block's closing brace OR the service's closing brace
+                    # If we only found options (and empty lines), it's likely the RPC's closing brace
+                    # If we found no options at all, it might be the service's closing brace (malformed RPC)
+                    
+                    has_non_empty_content = any(line.strip() for line in block_content)
+                    
+                    if option_count > 0 or has_non_empty_content:
+                        # We found content in this block, so this } belongs to the RPC
+                        if has_non_empty_content:
+                            # Keep the block with non-option content
+                            result.append(rpc_line)
+                            result.extend(block_content)
+                            result.append(block_line)
+                        else:
+                            # Only had options (now removed), convert to single-line
+                            rpc_single_line = rpc_line.rstrip().rstrip('{').rstrip()
+                            result.append(rpc_single_line + ';')
+                        i = j + 1
+                    else:
+                        # No options and no content found, this } likely belongs to service (malformed RPC)
+                        rpc_single_line = rpc_line.rstrip().rstrip('{').rstrip()
+                        result.append(rpc_single_line + ';')
+                        # Don't consume the }, continue from it
+                        i = j
+                    break
+                
+                # Check if this line is an option we want to remove
+                is_skip_option = any(pattern in block_stripped for pattern in SKIP_PATTERNS['option_blocks'])
+                
+                if is_skip_option:
+                    option_count += 1
+                else:
+                    # Keep lines that are not skippable options (including empty lines)
+                    block_content.append(block_line)
+                
+                j += 1
+            else:
+                # Reached end of file without closing brace, convert to single-line
+                rpc_single_line = rpc_line.rstrip().rstrip('{').rstrip()
+                result.append(rpc_single_line + ';')
+                i += 1
+            continue
+        
+        result.append(line)
+        i += 1
+    
+    return result
+
+
+def cleanup_empty_rpc_blocks(body_lines: List[str]) -> List[str]:
+    """
+    Remove empty {} blocks from RPC definitions.
+    Converts:
+      rpc Query(QueryRequest) returns (QueryResponse) {
+      }
+    to:
+      rpc Query(QueryRequest) returns (QueryResponse);
+    """
+    if not body_lines:
+        return body_lines
+    
+    result = []
+    i = 0
+    while i < len(body_lines):
+        line = body_lines[i]
+        stripped = line.strip()
+        
+        # Check if this is an RPC line ending with {
+        if 'rpc ' in stripped and stripped.endswith('{'):
+            # Look ahead to see if the next non-empty line is just }
+            j = i + 1
+            # Skip empty lines
+            while j < len(body_lines) and not body_lines[j].strip():
+                j += 1
+            
+            if j < len(body_lines) and body_lines[j].strip() == '}':
+                # Found empty RPC block, remove the { and the }
+                # Replace the rpc line to remove the { at the end
+                rpc_line = line.rstrip().rstrip('{').rstrip()
+                result.append(rpc_line + ';')
+                # Skip the closing }
+                i = j + 1
+                continue
+        
+        result.append(line)
+        i += 1
+    
+    return result
+
+
+def merge_proto_files(proto_contents: List[str], current_module: str = None, exclude_messages: List[str] = None, exclude_rpcs: List[str] = None) -> str:
+    """
+    Intelligently merge multiple proto files into one.
+    - Keep one license header
+    - Keep one syntax declaration
+    - Keep one java_package option
+    - Keep one package declaration
+    - Merge and deduplicate imports
+    - Concatenate all body content
+    """
+    if not proto_contents:
+        return ""
+    
+    parsed_files = [parse_proto_file(content) for content in proto_contents]
+    
+    # Build the merged content
+    merged = []
+    
+    # 1. License header (from first file)
+    if parsed_files[0]['license']:
+        merged.extend(parsed_files[0]['license'])
+        merged.append('')
+    
+    # 2. Syntax declaration (from first file)
+    if parsed_files[0]['syntax']:
+        merged.append(parsed_files[0]['syntax'])
+        merged.append('')
+    
+    # 3. Java package option (from first file)
+    if parsed_files[0]['java_package']:
+        merged.append(parsed_files[0]['java_package'])
+        merged.append('')
+    
+    # 4. Package declaration (from first file)
+    if parsed_files[0]['package']:
+        merged.append(parsed_files[0]['package'])
+        merged.append('')
+    
+    # 5. Merge, transform, and deduplicate imports
+    all_imports: Set[str] = set()
+    for parsed in parsed_files:
+        for imp in parsed['imports']:
+            # Transform the import path to the new merged file format
+            transformed_imp = transform_import_path(imp.strip())
+            
+            # Filter out self-imports (imports that reference the same module being merged)
+            if current_module and f'banyandb/v1/banyandb-{current_module}.proto' in transformed_imp:
+                continue
+            
+            all_imports.add(transformed_imp)
+    
+    if all_imports:
+        # Sort imports: google first, then validate, then banyandb
+        sorted_imports = sorted(all_imports, key=lambda x: (
+            0 if 'google/' in x else (1 if 'validate/' in x else 2),
+            x
+        ))
+        merged.extend(sorted_imports)
+        merged.append('')
+    
+    # 6. Concatenate bodies
+    for i, parsed in enumerate(parsed_files):
+        if parsed['body']:
+            # Remove leading empty lines from body
+            body_lines = parsed['body']
+            while body_lines and not body_lines[0].strip():
+                body_lines.pop(0)
+            
+            # Remove trailing empty lines from body
+            while body_lines and not body_lines[-1].strip():
+                body_lines.pop()
+            
+            # Filter out patterns defined in SKIP_PATTERNS (secondary filter for body content)
+            body_lines = [line for line in body_lines 
+                         if not any(line.strip().startswith(prefix) for prefix in SKIP_PATTERNS['line_prefixes'])
+                         and not any(pattern in line.strip() for pattern in SKIP_PATTERNS['line_contains'])]
+            
+            # Filter out excluded messages and RPCs
+            if exclude_messages or exclude_rpcs:
+                body_lines = filter_excluded_definitions(
+                    body_lines, 
+                    exclude_messages or [], 
+                    exclude_rpcs or []
+                )
+            
+            # Remove option blocks from inside RPC methods
+            body_lines = remove_options_from_rpc_blocks(body_lines)
+            
+            # Clean up empty RPC blocks
+            body_lines = cleanup_empty_rpc_blocks(body_lines)
+            
+            if body_lines:
+                if i > 0:
+                    # Add separator between files
+                    merged.append('')
+                merged.extend(body_lines)
+    
+    # Remove trailing empty lines
+    while merged and not merged[-1].strip():
+        merged.pop()
+    
+    # Ensure file ends with newline
+    return '\n'.join(merged) + '\n'
+
+
+def sync_module(branch: str, module: str, config: Dict, dry_run: bool = False) -> Tuple[bool, str]:
+    """
+    Sync a single module.
+    Returns (changed, message) tuple.
+    """
+    print(f"{Colors.CYAN}Processing module: {module}{Colors.RESET}")
+    
+    # Determine which files to fetch
+    if config['files'] == 'all':
+        try:
+            proto_files = fetch_directory_listing(branch, module)
+        except Exception as e:
+            return False, f"{Colors.RED}Error: {e}{Colors.RESET}"
+    else:
+        proto_files = config['files']
+    
+    print(f"  Files to sync: {', '.join(proto_files)}")
+    
+    # Fetch all proto files
+    fetched_contents = []
+    for filename in proto_files:
+        try:
+            print(f"  Fetching {filename}...", end=' ')
+            content = fetch_proto_file(branch, module, filename)
+            fetched_contents.append(content)
+            print(f"{Colors.GREEN}✓{Colors.RESET}")
+        except Exception as e:
+            print(f"{Colors.RED}✗{Colors.RESET}")
+            return False, f"{Colors.RED}Error fetching {filename}: {e}{Colors.RESET}"
+    
+    # Get exclusion lists for this module
+    exclude_config = EXCLUDE_LIST.get(module, {})
+    exclude_messages = exclude_config.get('messages', [])
+    exclude_rpcs = exclude_config.get('rpcs', [])
+    
+    if exclude_messages or exclude_rpcs:
+        print(f"  Applying exclusions: {len(exclude_messages)} messages, {len(exclude_rpcs)} RPCs")
+    
+    # Merge proto files
+    print(f"  Merging {len(fetched_contents)} files...")
+    merged_content = merge_proto_files(
+        fetched_contents, 
+        current_module=module,
+        exclude_messages=exclude_messages,
+        exclude_rpcs=exclude_rpcs
+    )
+    
+    # Determine output path
+    output_path = f"proto/banyandb/v1/banyandb-{module}.proto"
+    
+    # Check if file exists and compare
+    file_exists = os.path.exists(output_path)
+    changed = False
+    
+    if file_exists:
+        with open(output_path, 'r', encoding='utf-8') as f:
+            existing_content = f.read()
+        changed = existing_content != merged_content
+    else:
+        changed = True
+    
+    if changed:
+        print(f"  {Colors.YELLOW}Changes detected{Colors.RESET}")
+        
+        if not dry_run:
+            # Write the merged file
+            os.makedirs(os.path.dirname(output_path), exist_ok=True)
+            with open(output_path, 'w', encoding='utf-8') as f:
+                f.write(merged_content)
+            status = "updated" if file_exists else "created"
+            print(f"  {Colors.GREEN}✓ File {status}: {output_path}{Colors.RESET}")
+        else:
+            print(f"  {Colors.BLUE}[DRY RUN] Would update: {output_path}{Colors.RESET}")
+    else:
+        print(f"  {Colors.GREEN}✓ No changes needed{Colors.RESET}")
+    
+    return changed, output_path
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description='Sync proto files from Apache SkyWalking BanyanDB repository'
+    )
+    parser.add_argument(
+        '--branch',
+        default='main',
+        help='Branch or tag to sync from (default: main)'
+    )
+    parser.add_argument(
+        '--dry-run',
+        action='store_true',
+        help='Preview changes without writing files'
+    )
+    parser.add_argument(
+        '--module',
+        action='append',
+        help='Sync only specific module(s). Can be specified multiple times.'
+    )
+    parser.add_argument(
+        '--force',
+        action='store_true',
+        help='Skip confirmation prompts'
+    )
+    
+    args = parser.parse_args()
+    
+    # Determine which modules to sync
+    modules_to_sync = MODULES
+    if args.module:
+        modules_to_sync = {k: v for k, v in MODULES.items() if k in args.module}
+        if not modules_to_sync:
+            print(f"{Colors.RED}Error: No valid modules specified{Colors.RESET}")
+            print(f"Valid modules: {', '.join(MODULES.keys())}")
+            sys.exit(1)
+    
+    # Display configuration
+    print(f"{Colors.BOLD}=== Proto File Sync ==={Colors.RESET}")
+    print(f"Repository: {GITHUB_REPO}")
+    print(f"Branch: {args.branch}")
+    print(f"Modules: {', '.join(modules_to_sync.keys())}")
+    print(f"Mode: {'DRY RUN' if args.dry_run else 'LIVE'}")
+    print()
+    
+    # Confirmation prompt
+    if not args.force and not args.dry_run:
+        response = input(f"{Colors.YELLOW}Proceed with sync? [y/N]: {Colors.RESET}")
+        if response.lower() not in ['y', 'yes']:
+            print("Cancelled.")
+            sys.exit(0)
+        print()
+    
+    # Sync each module
+    results = []
+    for module, config in modules_to_sync.items():
+        changed, message = sync_module(args.branch, module, config, args.dry_run)
+        results.append((module, changed, message))
+        print()
+    
+    # Summary
+    print(f"{Colors.BOLD}=== Summary ==={Colors.RESET}")
+    changed_count = sum(1 for _, changed, _ in results if changed)
+    
+    for module, changed, message in results:
+        status = f"{Colors.YELLOW}CHANGED{Colors.RESET}" if changed else f"{Colors.GREEN}UNCHANGED{Colors.RESET}"
+        print(f"  {module}: {status}")
+    
+    print()
+    if args.dry_run:
+        print(f"{Colors.BLUE}Dry run complete. {changed_count} file(s) would be updated.{Colors.RESET}")
+    else:
+        print(f"{Colors.GREEN}Sync complete. {changed_count} file(s) updated.{Colors.RESET}")
+
+
+if __name__ == '__main__':
+    main()
+


(skywalking-banyandb-client-proto) 01/02: Add proto files for Banyandb API including common, database, measure, model, property, stream, and trace definitions

Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb-client-proto.git

commit 164447d7385441da4c0c7087b5faa4ff15c299a7
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Thu Dec 18 15:15:43 2025 +0000

    Add proto files for Banyandb API including common, database, measure, model, property, stream, and trace definitions
---
 proto/banyandb/v1/banyandb-common.proto   | 182 ++++++++
 proto/banyandb/v1/banyandb-database.proto | 742 ++++++++++++++++++++++++++++++
 proto/banyandb/v1/banyandb-measure.proto  | 219 +++++++++
 proto/banyandb/v1/banyandb-model.proto    | 198 ++++++++
 proto/banyandb/v1/banyandb-property.proto | 112 +++++
 proto/banyandb/v1/banyandb-stream.proto   | 130 ++++++
 proto/banyandb/v1/banyandb-trace.proto    | 122 +++++
 7 files changed, 1705 insertions(+)

diff --git a/proto/banyandb/v1/banyandb-common.proto b/proto/banyandb/v1/banyandb-common.proto
new file mode 100644
index 0000000..d4ace04
--- /dev/null
+++ b/proto/banyandb/v1/banyandb-common.proto
@@ -0,0 +1,182 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) 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.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.common.v1";
+
+package banyandb.common.v1;
+
+import "google/protobuf/timestamp.proto";
+import "validate/validate.proto";
+
+enum Catalog {
+  CATALOG_UNSPECIFIED = 0;
+  CATALOG_STREAM = 1;
+  CATALOG_MEASURE = 2;
+  CATALOG_PROPERTY = 3;
+  CATALOG_TRACE = 4;
+}
+
+// Metadata is for multi-tenant, multi-model use
+message Metadata {
+  // group contains a set of options, like retention policy, max
+  string group = 1;
+  // name of the entity
+  string name = 2;
+  // id is the unique identifier of the entity
+  // if id is not set, the system will generate a unique id
+  uint32 id = 3;
+  // readonly. create_revision is the revision of last creation on this key.
+  int64 create_revision = 4;
+  // readonly. mod_revision is the revision of last modification on this key.
+  int64 mod_revision = 5;
+}
+
+// IntervalRule is a structured duration
+message IntervalRule {
+  enum Unit {
+    UNIT_UNSPECIFIED = 0;
+    UNIT_HOUR = 1;
+    UNIT_DAY = 2;
+  }
+  // unit can only be UNIT_HOUR or UNIT_DAY
+  Unit unit = 1 [(validate.rules).enum.defined_only = true];
+  uint32 num = 2 [(validate.rules).uint32.gt = 0];
+}
+
+message LifecycleStage {
+  // The stage name (e.g., "warm", "cold").
+  // This should be a non-empty string.
+  string name = 1 [(validate.rules).string.min_len = 1];
+
+  // Number of shards allocated for this stage.
+  // Must be greater than zero.
+  uint32 shard_num = 2 [(validate.rules).uint32.gt = 0];
+
+  // Defines the interval for data segmentation in this stage.
+  // This is a required field and uses the IntervalRule structure.
+  IntervalRule segment_interval = 3 [(validate.rules).message.required = true];
+
+  // Specifies the time-to-live for data in this stage before moving to the next.
+  // This is also a required field using the IntervalRule structure.
+  IntervalRule ttl = 4 [(validate.rules).message.required = true];
+
+  // Node selector specifying target nodes for this stage.
+  // Optional; if provided, it must be a non-empty string.
+  string node_selector = 5 [(validate.rules).string.min_len = 1];
+
+  // Indicates whether segments that are no longer live should be closed.
+  bool close = 6;
+
+  // replicas is the number of replicas for this stage.
+  // This is an optional field and defaults to 0.
+  // A value of 0 means no replicas, while a value of 1 means one primary shard and one replica.
+  // Higher values indicate more replicas.
+  uint32 replicas = 7;
+}
+
+message ResourceOpts {
+  // shard_num is the number of shards
+  uint32 shard_num = 1 [(validate.rules).uint32.gt = 0];
+  // segment_interval indicates the length of a segment
+  IntervalRule segment_interval = 2;
+  // ttl indicates time to live, how long the data will be cached
+  IntervalRule ttl = 3;
+  // stages defines the ordered lifecycle stages. Data progresses through these stages sequentially.
+  repeated LifecycleStage stages = 4;
+  // default_stages is the name of the default stage
+  repeated string default_stages = 5;
+  // replicas is the number of replicas. This is used to ensure high availability and fault tolerance.
+  // This is an optional field and defaults to 0.
+  // A value of 0 means no replicas, while a value of 1 means one primary shard and one replica.
+  // Higher values indicate more replicas.
+  uint32 replicas = 6;
+}
+
+// Group is an internal object for Group management
+message Group {
+  // metadata define the group's identity
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // catalog denotes which type of data the group contains
+  common.v1.Catalog catalog = 2;
+  // resourceOpts indicates the structure of the underlying kv storage
+  ResourceOpts resource_opts = 3;
+  // updated_at indicates when resources of the group are updated
+  google.protobuf.Timestamp updated_at = 4;
+}
+
+// APIVersion is the version of the API
+message APIVersion {
+  // version is the version of the API
+  string version = 1;
+  // revision is the commit hash of the API
+  string revision = 2;
+}
+
+// GetAPIVersionRequest is the request for GetAPIVersion
+message GetAPIVersionRequest {
+  // empty
+}
+
+// GetAPIVersionResponse is the response for GetAPIVersion
+message GetAPIVersionResponse {
+  // version is the version of the API
+  APIVersion version = 1;
+}
+
+// Service is the service for the API
+service Service {
+  // GetAPIVersion returns the version of the API
+  rpc GetAPIVersion(GetAPIVersionRequest) returns (GetAPIVersionResponse);
+}
+
+// Trace is the top level message of a trace.
+message Trace {
+  // trace_id is the unique identifier of the trace.
+  string trace_id = 1;
+  // spans is a list of spans in the trace.
+  repeated Span spans = 2;
+  // error indicates whether the trace is an error trace.
+  bool error = 3;
+}
+
+// Span is the basic unit of a trace.
+message Span {
+  // start_time is the start time of the span.
+  google.protobuf.Timestamp start_time = 1;
+  // end_time is the end time of the span.
+  google.protobuf.Timestamp end_time = 2;
+  // error indicates whether the span is an error span.
+  bool error = 3;
+  // tags is a list of tags of the span.
+  repeated Tag tags = 4;
+  // message is the message generated by the span.
+  string message = 5;
+  // children is a list of child spans of the span.
+  repeated Span children = 6;
+  // duration is the duration of the span.
+  int64 duration = 7;
+}
+
+// Tag is the key-value pair of a span.
+message Tag {
+  // key is the key of the tag.
+  string key = 1;
+  // value is the value of the tag.
+  string value = 2;
+}
diff --git a/proto/banyandb/v1/banyandb-database.proto b/proto/banyandb/v1/banyandb-database.proto
new file mode 100644
index 0000000..bd8242a
--- /dev/null
+++ b/proto/banyandb/v1/banyandb-database.proto
@@ -0,0 +1,742 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) 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.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.database.v1";
+
+package banyandb.database.v1;
+
+import "google/protobuf/timestamp.proto";
+import "validate/validate.proto";
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
+
+enum TagType {
+  TAG_TYPE_UNSPECIFIED = 0;
+  TAG_TYPE_STRING = 1;
+  TAG_TYPE_INT = 2;
+  TAG_TYPE_STRING_ARRAY = 3;
+  TAG_TYPE_INT_ARRAY = 4;
+  TAG_TYPE_DATA_BINARY = 5;
+  TAG_TYPE_TIMESTAMP = 6;
+}
+
+message TagFamilySpec {
+  string name = 1 [(validate.rules).string.min_len = 1];
+  // tags defines accepted tags
+  repeated TagSpec tags = 2 [(validate.rules).repeated.min_items = 1];
+}
+
+message TagSpec {
+  string name = 1 [(validate.rules).string.min_len = 1];
+  TagType type = 2 [(validate.rules).enum.defined_only = true];
+}
+
+// Stream intends to store streaming data, for example, traces or logs
+message Stream {
+  // metadata is the identity of a trace series
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // tag_families
+  repeated TagFamilySpec tag_families = 2 [(validate.rules).repeated.min_items = 1];
+  // entity indicates how to generate a series and shard a stream
+  Entity entity = 3 [(validate.rules).message.required = true];
+  // updated_at indicates when the stream is updated
+  google.protobuf.Timestamp updated_at = 4;
+}
+
+message Entity {
+  repeated string tag_names = 1 [(validate.rules).repeated.min_items = 1];
+}
+
+message ShardingKey {
+  repeated string tag_names = 1 [(validate.rules).repeated.min_items = 1];
+}
+
+enum FieldType {
+  FIELD_TYPE_UNSPECIFIED = 0;
+  FIELD_TYPE_STRING = 1;
+  FIELD_TYPE_INT = 2;
+  FIELD_TYPE_DATA_BINARY = 3;
+  FIELD_TYPE_FLOAT = 4;
+}
+
+enum EncodingMethod {
+  ENCODING_METHOD_UNSPECIFIED = 0;
+  ENCODING_METHOD_GORILLA = 1;
+}
+
+enum CompressionMethod {
+  COMPRESSION_METHOD_UNSPECIFIED = 0;
+  COMPRESSION_METHOD_ZSTD = 1;
+}
+
+// FieldSpec is the specification of field
+message FieldSpec {
+  // name is the identity of a field
+  string name = 1 [(validate.rules).string.min_len = 1];
+  // field_type denotes the type of field value
+  FieldType field_type = 2 [(validate.rules).enum.defined_only = true];
+  // encoding_method indicates how to encode data during writing
+  EncodingMethod encoding_method = 3 [(validate.rules).enum.defined_only = true];
+  // compression_method indicates how to compress data during writing
+  CompressionMethod compression_method = 4 [(validate.rules).enum.defined_only = true];
+}
+
+// Measure intends to store data point
+message Measure {
+  // metadata is the identity of a measure
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // tag_families are for filter measures
+  repeated TagFamilySpec tag_families = 2 [(validate.rules).repeated.min_items = 1];
+  // fields denote measure values
+  repeated FieldSpec fields = 3;
+  // entity indicates which tags will be to generate a series and shard a measure
+  Entity entity = 4 [(validate.rules).message.required = true];
+  // interval indicates how frequently to send a data point
+  // valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d".
+  string interval = 5;
+  // updated_at indicates when the measure is updated
+  google.protobuf.Timestamp updated_at = 6;
+  // index_mode specifies whether the data should be stored exclusively in the index,
+  // meaning it will not be stored in the data storage system.
+  bool index_mode = 7;
+  // sharding_key determines the distribution of TopN-related data.
+  ShardingKey sharding_key = 8;
+}
+
+// TopNAggregation generates offline TopN statistics for a measure's TopN approximation
+message TopNAggregation {
+  // metadata is the identity of an aggregation
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // source_measure denotes the data source of this aggregation
+  common.v1.Metadata source_measure = 2 [(validate.rules).message.required = true];
+  // field_name is the name of field used for ranking
+  string field_name = 3 [(validate.rules).string.min_len = 1];
+  // field_value_sort indicates how to sort fields
+  // ASC: bottomN
+  // DESC: topN
+  // UNSPECIFIED: topN + bottomN
+  // todo validate plugin exist bug https://github.com/bufbuild/protoc-gen-validate/issues/672
+  model.v1.Sort field_value_sort = 4;
+  // group_by_tag_names groups data points into statistical counters
+  repeated string group_by_tag_names = 5;
+  // criteria select partial data points from measure
+  model.v1.Criteria criteria = 6;
+  // counters_number sets the number of counters to be tracked. The default value is 1000
+  int32 counters_number = 7;
+  // lru_size defines how much entry is allowed to be maintained in the memory
+  int32 lru_size = 8;
+  // updated_at indicates when the measure is updated
+  google.protobuf.Timestamp updated_at = 9;
+}
+
+// IndexRule defines how to generate indices based on tags and the index type
+// IndexRule should bind to a subject through an IndexRuleBinding to generate proper indices.
+message IndexRule {
+  // metadata define the rule's identity
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // tags are the combination that refers to an indexed object
+  // If the elements in tags are more than 1, the object will generate a multi-tag index
+  // Caveat: All tags in a multi-tag MUST have an identical IndexType
+  repeated string tags = 2 [(validate.rules).repeated.min_items = 1];
+  // Type determine the index structure under the hood
+  enum Type {
+    TYPE_UNSPECIFIED = 0;
+    TYPE_INVERTED = 1;
+    TYPE_SKIPPING = 2;
+    // TYPE_TREE is a tree index, which is used for storing hierarchical data.
+    TYPE_TREE = 3;
+  }
+  // type is the IndexType of this IndexObject.
+  Type type = 3 [(validate.rules).enum.defined_only = true];
+  // updated_at indicates when the IndexRule is updated
+  google.protobuf.Timestamp updated_at = 4;
+
+  // analyzer analyzes tag value to support the full-text searching for TYPE_INVERTED indices.
+  // available analyzers are:
+  // - "standard" provides grammar based tokenization
+  // - "simple" breaks text into tokens at any non-letter character,
+  //            such as numbers, spaces, hyphens and apostrophes, discards non-letter characters,
+  //            and changes uppercase to lowercase.
+  // - "keyword" is a “noop” analyzer which returns the entire input string as a single token.
+  // - "url" breaks test into tokens at any non-letter and non-digit character.
+  string analyzer = 5;
+  // no_sort indicates whether the index is not for sorting.
+  bool no_sort = 6;
+}
+
+// Subject defines which stream or measure would generate indices
+message Subject {
+  // catalog is where the subject belongs to
+  // todo validate plugin exist bug https://github.com/bufbuild/protoc-gen-validate/issues/672
+  common.v1.Catalog catalog = 1;
+  // name refers to a stream or measure in a particular catalog
+  string name = 2 [(validate.rules).string.min_len = 1];
+}
+
+// IndexRuleBinding is a bridge to connect severalIndexRules to a subject
+// This binding is valid between begin_at_nanoseconds and expire_at_nanoseconds, that provides flexible strategies
+// to control how to generate time series indices.
+message IndexRuleBinding {
+  // metadata is the identity of this binding
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // rules refers to the IndexRule
+  repeated string rules = 2 [(validate.rules).repeated.min_items = 1];
+  // subject indicates the subject of binding action
+  Subject subject = 3 [(validate.rules).message.required = true];
+  // begin_at_nanoseconds is the timestamp, after which the binding will be active
+  google.protobuf.Timestamp begin_at = 4 [(validate.rules).timestamp.required = true];
+  // expire_at_nanoseconds it the timestamp, after which the binding will be inactive
+  // expire_at_nanoseconds must be larger than begin_at_nanoseconds
+  google.protobuf.Timestamp expire_at = 5 [(validate.rules).timestamp.required = true];
+  // updated_at indicates when the IndexRuleBinding is updated
+  google.protobuf.Timestamp updated_at = 6;
+}
+
+// Property stores the user defined data
+message Property {
+  // metadata is the identity of a property
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // tag stores the content of a property
+  repeated TagSpec tags = 2 [(validate.rules).repeated.min_items = 1];
+  // updated_at indicates when the property is updated
+  google.protobuf.Timestamp updated_at = 6;
+}
+
+// TraceTagSpec defines the specification of a tag in a trace.
+message TraceTagSpec {
+  // name is the name of the tag.
+  string name = 1 [(validate.rules).string.min_len = 1];
+  // type is the type of the tag.
+  TagType type = 2 [(validate.rules).enum.defined_only = true];
+}
+
+// Trace defines a tracing-specific storage resource.
+// It is suitable for storing traces and spans.
+// The name of a Trace is a logical namespace within a group,
+// while the group of a Trace corresponds to a physical directory.
+message Trace {
+  // metadata is the identity of the trace resource.
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // tags are the specification of tags.
+  repeated TraceTagSpec tags = 2 [(validate.rules).repeated.min_items = 1];
+  // trace_id_tag_name is the name of the tag that stores the trace ID.
+  string trace_id_tag_name = 3 [(validate.rules).string.min_len = 1];
+  // timestamp_tag_name is the name of the tag that stores the timestamp.
+  string timestamp_tag_name = 4 [(validate.rules).string.min_len = 1];
+  // updated_at indicates when the trace resource is updated.
+  google.protobuf.Timestamp updated_at = 5;
+  // span_id_tag_name is the name of the tag that stores the span ID.
+  string span_id_tag_name = 6 [(validate.rules).string.min_len = 1];
+}
+
+message StreamRegistryServiceCreateRequest {
+  banyandb.database.v1.Stream stream = 1;
+}
+
+message StreamRegistryServiceCreateResponse {
+  int64 mod_revision = 1;
+}
+
+message StreamRegistryServiceUpdateRequest {
+  banyandb.database.v1.Stream stream = 1;
+}
+
+message StreamRegistryServiceUpdateResponse {
+  int64 mod_revision = 1;
+}
+
+message StreamRegistryServiceDeleteRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message StreamRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message StreamRegistryServiceGetRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message StreamRegistryServiceGetResponse {
+  banyandb.database.v1.Stream stream = 1;
+}
+
+message StreamRegistryServiceExistRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message StreamRegistryServiceExistResponse {
+  bool has_group = 1;
+  bool has_stream = 2;
+}
+
+message StreamRegistryServiceListRequest {
+  string group = 1;
+}
+
+message StreamRegistryServiceListResponse {
+  repeated banyandb.database.v1.Stream stream = 1;
+}
+
+service StreamRegistryService {
+  rpc Create(StreamRegistryServiceCreateRequest) returns (StreamRegistryServiceCreateResponse);
+  rpc Update(StreamRegistryServiceUpdateRequest) returns (StreamRegistryServiceUpdateResponse);
+  rpc Delete(StreamRegistryServiceDeleteRequest) returns (StreamRegistryServiceDeleteResponse);
+
+  rpc Get(StreamRegistryServiceGetRequest) returns (StreamRegistryServiceGetResponse);
+
+  rpc List(StreamRegistryServiceListRequest) returns (StreamRegistryServiceListResponse);
+
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(StreamRegistryServiceExistRequest) returns (StreamRegistryServiceExistResponse);
+}
+
+message IndexRuleBindingRegistryServiceCreateRequest {
+  banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
+}
+
+message IndexRuleBindingRegistryServiceCreateResponse {}
+
+message IndexRuleBindingRegistryServiceUpdateRequest {
+  banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
+}
+
+message IndexRuleBindingRegistryServiceUpdateResponse {}
+
+message IndexRuleBindingRegistryServiceDeleteRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message IndexRuleBindingRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message IndexRuleBindingRegistryServiceGetRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message IndexRuleBindingRegistryServiceGetResponse {
+  banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
+}
+
+message IndexRuleBindingRegistryServiceListRequest {
+  string group = 1;
+}
+
+message IndexRuleBindingRegistryServiceListResponse {
+  repeated banyandb.database.v1.IndexRuleBinding index_rule_binding = 1;
+}
+
+message IndexRuleBindingRegistryServiceExistRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message IndexRuleBindingRegistryServiceExistResponse {
+  bool has_group = 1;
+  bool has_index_rule_binding = 2;
+}
+
+service IndexRuleBindingRegistryService {
+  rpc Create(IndexRuleBindingRegistryServiceCreateRequest) returns (IndexRuleBindingRegistryServiceCreateResponse);
+  rpc Update(IndexRuleBindingRegistryServiceUpdateRequest) returns (IndexRuleBindingRegistryServiceUpdateResponse);
+  rpc Delete(IndexRuleBindingRegistryServiceDeleteRequest) returns (IndexRuleBindingRegistryServiceDeleteResponse);
+
+  rpc Get(IndexRuleBindingRegistryServiceGetRequest) returns (IndexRuleBindingRegistryServiceGetResponse);
+
+  rpc List(IndexRuleBindingRegistryServiceListRequest) returns (IndexRuleBindingRegistryServiceListResponse);
+
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(IndexRuleBindingRegistryServiceExistRequest) returns (IndexRuleBindingRegistryServiceExistResponse);
+}
+
+message IndexRuleRegistryServiceCreateRequest {
+  banyandb.database.v1.IndexRule index_rule = 1;
+}
+
+message IndexRuleRegistryServiceCreateResponse {}
+
+message IndexRuleRegistryServiceUpdateRequest {
+  banyandb.database.v1.IndexRule index_rule = 1;
+}
+
+message IndexRuleRegistryServiceUpdateResponse {}
+
+message IndexRuleRegistryServiceDeleteRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message IndexRuleRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message IndexRuleRegistryServiceGetRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message IndexRuleRegistryServiceGetResponse {
+  banyandb.database.v1.IndexRule index_rule = 1;
+}
+
+message IndexRuleRegistryServiceListRequest {
+  string group = 1;
+}
+
+message IndexRuleRegistryServiceListResponse {
+  repeated banyandb.database.v1.IndexRule index_rule = 1;
+}
+
+message IndexRuleRegistryServiceExistRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message IndexRuleRegistryServiceExistResponse {
+  bool has_group = 1;
+  bool has_index_rule = 2;
+}
+
+service IndexRuleRegistryService {
+  rpc Create(IndexRuleRegistryServiceCreateRequest) returns (IndexRuleRegistryServiceCreateResponse);
+  rpc Update(IndexRuleRegistryServiceUpdateRequest) returns (IndexRuleRegistryServiceUpdateResponse);
+  rpc Delete(IndexRuleRegistryServiceDeleteRequest) returns (IndexRuleRegistryServiceDeleteResponse);
+
+  rpc Get(IndexRuleRegistryServiceGetRequest) returns (IndexRuleRegistryServiceGetResponse);
+
+  rpc List(IndexRuleRegistryServiceListRequest) returns (IndexRuleRegistryServiceListResponse);
+
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(IndexRuleRegistryServiceExistRequest) returns (IndexRuleRegistryServiceExistResponse);
+}
+
+message MeasureRegistryServiceCreateRequest {
+  banyandb.database.v1.Measure measure = 1;
+}
+
+message MeasureRegistryServiceCreateResponse {
+  int64 mod_revision = 1;
+}
+
+message MeasureRegistryServiceUpdateRequest {
+  banyandb.database.v1.Measure measure = 1;
+}
+
+message MeasureRegistryServiceUpdateResponse {
+  int64 mod_revision = 1;
+}
+
+message MeasureRegistryServiceDeleteRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message MeasureRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message MeasureRegistryServiceGetRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message MeasureRegistryServiceGetResponse {
+  banyandb.database.v1.Measure measure = 1;
+}
+
+message MeasureRegistryServiceListRequest {
+  string group = 1;
+}
+
+message MeasureRegistryServiceListResponse {
+  repeated banyandb.database.v1.Measure measure = 1;
+}
+
+message MeasureRegistryServiceExistRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message MeasureRegistryServiceExistResponse {
+  bool has_group = 1;
+  bool has_measure = 2;
+}
+
+service MeasureRegistryService {
+  rpc Create(MeasureRegistryServiceCreateRequest) returns (MeasureRegistryServiceCreateResponse);
+  rpc Update(MeasureRegistryServiceUpdateRequest) returns (MeasureRegistryServiceUpdateResponse);
+  rpc Delete(MeasureRegistryServiceDeleteRequest) returns (MeasureRegistryServiceDeleteResponse);
+
+  rpc Get(MeasureRegistryServiceGetRequest) returns (MeasureRegistryServiceGetResponse);
+
+  rpc List(MeasureRegistryServiceListRequest) returns (MeasureRegistryServiceListResponse);
+
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(MeasureRegistryServiceExistRequest) returns (MeasureRegistryServiceExistResponse);
+}
+
+message GroupRegistryServiceCreateRequest {
+  banyandb.common.v1.Group group = 1;
+}
+
+message GroupRegistryServiceCreateResponse {}
+
+message GroupRegistryServiceUpdateRequest {
+  banyandb.common.v1.Group group = 1;
+}
+
+message GroupRegistryServiceUpdateResponse {}
+
+message GroupRegistryServiceDeleteRequest {
+  string group = 1;
+}
+
+message GroupRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message GroupRegistryServiceGetRequest {
+  string group = 1;
+}
+
+message GroupRegistryServiceGetResponse {
+  banyandb.common.v1.Group group = 1;
+}
+
+message GroupRegistryServiceListRequest {}
+
+message GroupRegistryServiceListResponse {
+  repeated banyandb.common.v1.Group group = 1;
+}
+
+message GroupRegistryServiceExistRequest {
+  string group = 1;
+}
+
+message GroupRegistryServiceExistResponse {
+  bool has_group = 1;
+}
+
+service GroupRegistryService {
+  rpc Create(GroupRegistryServiceCreateRequest) returns (GroupRegistryServiceCreateResponse);
+  rpc Update(GroupRegistryServiceUpdateRequest) returns (GroupRegistryServiceUpdateResponse);
+  rpc Delete(GroupRegistryServiceDeleteRequest) returns (GroupRegistryServiceDeleteResponse);
+
+  rpc Get(GroupRegistryServiceGetRequest) returns (GroupRegistryServiceGetResponse);
+
+  rpc List(GroupRegistryServiceListRequest) returns (GroupRegistryServiceListResponse);
+
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(GroupRegistryServiceExistRequest) returns (GroupRegistryServiceExistResponse);
+}
+
+message TopNAggregationRegistryServiceCreateRequest {
+  banyandb.database.v1.TopNAggregation top_n_aggregation = 1;
+}
+
+message TopNAggregationRegistryServiceCreateResponse {}
+
+message TopNAggregationRegistryServiceUpdateRequest {
+  banyandb.database.v1.TopNAggregation top_n_aggregation = 1;
+}
+
+message TopNAggregationRegistryServiceUpdateResponse {}
+
+message TopNAggregationRegistryServiceDeleteRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message TopNAggregationRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message TopNAggregationRegistryServiceGetRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message TopNAggregationRegistryServiceGetResponse {
+  banyandb.database.v1.TopNAggregation top_n_aggregation = 1;
+}
+
+message TopNAggregationRegistryServiceListRequest {
+  string group = 1;
+}
+
+message TopNAggregationRegistryServiceListResponse {
+  repeated banyandb.database.v1.TopNAggregation top_n_aggregation = 1;
+}
+
+message TopNAggregationRegistryServiceExistRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message TopNAggregationRegistryServiceExistResponse {
+  bool has_group = 1;
+  bool has_top_n_aggregation = 2;
+}
+
+service TopNAggregationRegistryService {
+  rpc Create(TopNAggregationRegistryServiceCreateRequest) returns (TopNAggregationRegistryServiceCreateResponse);
+  rpc Update(TopNAggregationRegistryServiceUpdateRequest) returns (TopNAggregationRegistryServiceUpdateResponse);
+  rpc Delete(TopNAggregationRegistryServiceDeleteRequest) returns (TopNAggregationRegistryServiceDeleteResponse);
+  rpc Get(TopNAggregationRegistryServiceGetRequest) returns (TopNAggregationRegistryServiceGetResponse);
+  rpc List(TopNAggregationRegistryServiceListRequest) returns (TopNAggregationRegistryServiceListResponse);
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(TopNAggregationRegistryServiceExistRequest) returns (TopNAggregationRegistryServiceExistResponse);
+}
+
+message SnapshotRequest {
+  message Group {
+    common.v1.Catalog catalog = 1;
+    string group = 2;
+  }
+  repeated Group groups = 1;
+}
+
+message Snapshot {
+  common.v1.Catalog catalog = 1;
+  string name = 2;
+  string error = 3;
+}
+
+message SnapshotResponse {
+  repeated Snapshot snapshots = 1;
+}
+
+service SnapshotService {
+  rpc Snapshot(SnapshotRequest) returns (SnapshotResponse);
+}
+
+message PropertyRegistryServiceCreateRequest {
+  banyandb.database.v1.Property property = 1;
+}
+
+message PropertyRegistryServiceCreateResponse {
+  int64 mod_revision = 1;
+}
+
+message PropertyRegistryServiceUpdateRequest {
+  banyandb.database.v1.Property property = 1;
+}
+
+message PropertyRegistryServiceUpdateResponse {
+  int64 mod_revision = 1;
+}
+
+message PropertyRegistryServiceDeleteRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message PropertyRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message PropertyRegistryServiceGetRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message PropertyRegistryServiceGetResponse {
+  banyandb.database.v1.Property property = 1;
+}
+
+message PropertyRegistryServiceListRequest {
+  string group = 1;
+}
+
+message PropertyRegistryServiceListResponse {
+  repeated banyandb.database.v1.Property properties = 1;
+}
+
+message PropertyRegistryServiceExistRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message PropertyRegistryServiceExistResponse {
+  bool has_group = 1;
+  bool has_property = 2;
+}
+
+service PropertyRegistryService {
+  rpc Create(PropertyRegistryServiceCreateRequest) returns (PropertyRegistryServiceCreateResponse);
+  rpc Update(PropertyRegistryServiceUpdateRequest) returns (PropertyRegistryServiceUpdateResponse);
+  rpc Delete(PropertyRegistryServiceDeleteRequest) returns (PropertyRegistryServiceDeleteResponse);
+
+  rpc Get(PropertyRegistryServiceGetRequest) returns (PropertyRegistryServiceGetResponse);
+
+  rpc List(PropertyRegistryServiceListRequest) returns (PropertyRegistryServiceListResponse);
+
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(PropertyRegistryServiceExistRequest) returns (PropertyRegistryServiceExistResponse);
+}
+
+message TraceRegistryServiceCreateRequest {
+  banyandb.database.v1.Trace trace = 1;
+}
+
+message TraceRegistryServiceCreateResponse {
+  int64 mod_revision = 1;
+}
+
+message TraceRegistryServiceUpdateRequest {
+  banyandb.database.v1.Trace trace = 1;
+}
+
+message TraceRegistryServiceUpdateResponse {
+  int64 mod_revision = 1;
+}
+
+message TraceRegistryServiceDeleteRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message TraceRegistryServiceDeleteResponse {
+  bool deleted = 1;
+}
+
+message TraceRegistryServiceGetRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message TraceRegistryServiceGetResponse {
+  banyandb.database.v1.Trace trace = 1;
+}
+
+message TraceRegistryServiceExistRequest {
+  banyandb.common.v1.Metadata metadata = 1;
+}
+
+message TraceRegistryServiceExistResponse {
+  bool has_group = 1;
+  bool has_trace = 2;
+}
+
+message TraceRegistryServiceListRequest {
+  string group = 1;
+}
+
+message TraceRegistryServiceListResponse {
+  repeated banyandb.database.v1.Trace trace = 1;
+}
+
+service TraceRegistryService {
+  rpc Create(TraceRegistryServiceCreateRequest) returns (TraceRegistryServiceCreateResponse);
+  rpc Update(TraceRegistryServiceUpdateRequest) returns (TraceRegistryServiceUpdateResponse);
+  rpc Delete(TraceRegistryServiceDeleteRequest) returns (TraceRegistryServiceDeleteResponse);
+
+  rpc Get(TraceRegistryServiceGetRequest) returns (TraceRegistryServiceGetResponse);
+
+  rpc List(TraceRegistryServiceListRequest) returns (TraceRegistryServiceListResponse);
+
+  // Exist doesn't expose an HTTP endpoint. Please use HEAD method to touch Get instead
+  rpc Exist(TraceRegistryServiceExistRequest) returns (TraceRegistryServiceExistResponse);
+}
diff --git a/proto/banyandb/v1/banyandb-measure.proto b/proto/banyandb/v1/banyandb-measure.proto
new file mode 100644
index 0000000..f8c4f91
--- /dev/null
+++ b/proto/banyandb/v1/banyandb-measure.proto
@@ -0,0 +1,219 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) 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.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.measure.v1";
+
+package banyandb.measure.v1;
+
+import "google/protobuf/timestamp.proto";
+import "validate/validate.proto";
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
+
+// DataPoint is stored in Measures
+message DataPoint {
+  // timestamp is in the timeunit of milliseconds.
+  google.protobuf.Timestamp timestamp = 1;
+  // tag_families contains tags selected in the projection
+  repeated model.v1.TagFamily tag_families = 2;
+  message Field {
+    string name = 1;
+    model.v1.FieldValue value = 2;
+  }
+  // fields contains fields selected in the projection
+  repeated Field fields = 3;
+  // sid is the series id of the data point
+  uint64 sid = 4;
+  // version is the version of the data point in a series
+  // sid, timestamp and version are used to identify a data point
+  int64 version = 5;
+}
+
+// QueryResponse is the response for a query to the Query module.
+message QueryResponse {
+  // data_points are the actual data returned
+  repeated DataPoint data_points = 1;
+  // trace contains the trace information of the query when trace is enabled
+  common.v1.Trace trace = 2;
+}
+
+// QueryRequest is the request contract for query.
+message QueryRequest {
+  // groups indicate where the data points are stored.
+  repeated string groups = 1 [(validate.rules).repeated.min_items = 1];
+  // name is the identity of a measure.
+  string name = 2 [(validate.rules).string.min_len = 1];
+  // time_range is a range query with begin/end time of entities in the timeunit of milliseconds.
+  model.v1.TimeRange time_range = 3 [(validate.rules).message.required = true];
+  // tag_families are indexed.
+  model.v1.Criteria criteria = 4;
+  // tag_projection can be used to select tags of the data points in the response
+  model.v1.TagProjection tag_projection = 5;
+  message FieldProjection {
+    repeated string names = 1;
+  }
+  // field_projection can be used to select fields of the data points in the response
+  FieldProjection field_projection = 6;
+  message GroupBy {
+    // tag_projection must be a subset of the tag_projection of QueryRequest
+    model.v1.TagProjection tag_projection = 1;
+    // field_name must be one of fields indicated by field_projection
+    string field_name = 2;
+  }
+  // group_by groups data points based on their field value for a specific tag and use field_name as the projection name
+  GroupBy group_by = 7;
+  message Aggregation {
+    model.v1.AggregationFunction function = 1;
+    // field_name must be one of files indicated by the field_projection
+    string field_name = 2;
+  }
+  // agg aggregates data points based on a field
+  Aggregation agg = 8;
+  message Top {
+    // number set the how many items should be returned
+    int32 number = 1;
+    // field_name must be one of files indicated by the field_projection
+    string field_name = 2;
+    // field_value_sort indicates how to sort fields
+    // ASC: bottomN
+    // DESC: topN
+    // UNSPECIFIED: topN
+    model.v1.Sort field_value_sort = 3;
+  }
+  // top limits the result based on a particular field.
+  // If order_by is specified, top sorts the dataset based on order_by's output
+  Top top = 9;
+  // offset is used to support pagination, together with the following limit.
+  // If top is specified, offset processes the dataset based on top's output
+  uint32 offset = 10;
+  // limit is used to impose a boundary on the number of records being returned.
+  // If top is specified, limit processes the dataset based on top's output
+  uint32 limit = 11;
+  // order_by is given to specify the sort for a tag.
+  model.v1.QueryOrder order_by = 12;
+  // trace is used to enable trace for the query
+  bool trace = 13;
+  // stages is used to specify the stage of the data points in the lifecycle
+  repeated string stages = 14;
+  // rewriteAggTopNResult will rewrite agg result to raw data
+  bool rewrite_agg_top_n_result = 15;
+}
+
+
+
+service MeasureService {
+  rpc Query(QueryRequest) returns (QueryResponse);
+  rpc Write(stream WriteRequest) returns (stream WriteResponse);
+  rpc TopN(TopNRequest) returns (TopNResponse);
+}
+
+// TopNList contains a series of topN items
+message TopNList {
+  // timestamp is in the timeunit of milliseconds.
+  google.protobuf.Timestamp timestamp = 1;
+  message Item {
+    repeated model.v1.Tag entity = 1;
+    model.v1.FieldValue value = 2;
+  }
+  // items contains top-n items in a list
+  repeated Item items = 2;
+}
+
+// TopNResponse is the response for a query to the Query module.
+message TopNResponse {
+  // lists contain a series topN lists ranked by timestamp
+  // if agg_func in query request is specified, lists' size should be one.
+  repeated TopNList lists = 1;
+  // trace contains the trace information of the query when trace is enabled
+  common.v1.Trace trace = 2;
+}
+
+// TopNRequest is the request contract for query.
+message TopNRequest {
+  // groups indicate where the data points are stored.
+  repeated string groups = 1 [(validate.rules).repeated.min_items = 1];
+  // name is the identity of a measure.
+  string name = 2 [(validate.rules).string.min_len = 1];
+  // time_range is a range query with begin/end time of entities in the timeunit of milliseconds.
+  model.v1.TimeRange time_range = 3 [(validate.rules).message.required = true];
+  // top_n set the how many items should be returned in each list.
+  int32 top_n = 4 [(validate.rules).int32.gt = 0];
+  // agg aggregates lists grouped by field names in the time_range
+  model.v1.AggregationFunction agg = 5;
+  // criteria select counters. Only equals are acceptable.
+  repeated model.v1.Condition conditions = 6;
+  // field_value_sort indicates how to sort fields
+  model.v1.Sort field_value_sort = 7;
+  // trace is used to enable trace for the query
+  bool trace = 8;
+  // stages is used to specify the stage of the data points in the lifecycle
+  repeated string stages = 9;
+}
+
+// TagFamilySpec defines the specification of a tag family.
+message TagFamilySpec {
+  // name of the tag family
+  string name = 1;
+  // names of tags in the tag family
+  repeated string tag_names = 2;
+}
+
+// DataPointSpec defines the specification of a data point.
+message DataPointSpec {
+  // the tag family specification
+  repeated TagFamilySpec tag_family_spec = 1;
+  // the field names
+  repeated string field_names = 2;
+}
+
+// DataPointValue is the data point for writing. It only contains values.
+message DataPointValue {
+  // timestamp is in the timeunit of milliseconds.
+  google.protobuf.Timestamp timestamp = 1 [(validate.rules).timestamp.required = true];
+  // the order of tag_families' items match DataPointSpec
+  repeated model.v1.TagFamilyForWrite tag_families = 2 [(validate.rules).repeated.min_items = 1];
+  // the order of fields match DataPointSpec
+  repeated model.v1.FieldValue fields = 3;
+  // the version of the data point
+  int64 version = 4;
+}
+
+// WriteRequest is the request contract for write
+message WriteRequest {
+  // the metadata is required only for the first request of gRPC stream.
+  common.v1.Metadata metadata = 1;
+  // the data_point is required.
+  DataPointValue data_point = 2 [(validate.rules).message.required = true];
+  // the message_id is required.
+  uint64 message_id = 3 [(validate.rules).uint64.gt = 0];
+  // the data point specification.
+  // If this is not set with the indicated metadata, use the schema definition.
+  // If this is not set, use the existing spec declaration from previous requests in the current gRPC stream.
+  DataPointSpec data_point_spec = 4;
+}
+
+// WriteResponse is the response contract for write
+message WriteResponse {
+  // the message_id from request.
+  uint64 message_id = 1 [(validate.rules).uint64.gt = 0];
+  // status indicates the request processing result
+  string status = 2;
+  // the metadata from request when request fails
+  common.v1.Metadata metadata = 3;
+}
diff --git a/proto/banyandb/v1/banyandb-model.proto b/proto/banyandb/v1/banyandb-model.proto
new file mode 100644
index 0000000..e6066fc
--- /dev/null
+++ b/proto/banyandb/v1/banyandb-model.proto
@@ -0,0 +1,198 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) 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.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.model.v1";
+
+package banyandb.model.v1;
+
+import "google/protobuf/struct.proto";
+import "google/protobuf/timestamp.proto";
+
+message Str {
+  string value = 1;
+}
+
+message Int {
+  int64 value = 1;
+}
+
+message Float {
+  double value = 1;
+}
+
+message StrArray {
+  repeated string value = 1;
+}
+
+message IntArray {
+  repeated int64 value = 1;
+}
+
+message TagValue {
+  oneof value {
+    google.protobuf.NullValue null = 1;
+    Str str = 2;
+    StrArray str_array = 3;
+    Int int = 4;
+    IntArray int_array = 5;
+    bytes binary_data = 6;
+    google.protobuf.Timestamp timestamp = 7;
+  }
+}
+
+message TagFamilyForWrite {
+  repeated TagValue tags = 1;
+}
+
+message FieldValue {
+  oneof value {
+    google.protobuf.NullValue null = 1;
+    model.v1.Str str = 2;
+    model.v1.Int int = 3;
+    bytes binary_data = 4;
+    model.v1.Float float = 5;
+  }
+}
+
+enum AggregationFunction {
+  AGGREGATION_FUNCTION_UNSPECIFIED = 0;
+  AGGREGATION_FUNCTION_MEAN = 1;
+  AGGREGATION_FUNCTION_MAX = 2;
+  AGGREGATION_FUNCTION_MIN = 3;
+  AGGREGATION_FUNCTION_COUNT = 4;
+  AGGREGATION_FUNCTION_SUM = 5;
+}
+
+// Pair is the building block of a record which is equivalent to a key-value pair.
+// In the context of Trace, it could be metadata of a trace such as service_name, service_instance, etc.
+// Besides, other tags are organized in key-value pair in the underlying storage layer.
+// One should notice that the values can be a multi-value.
+message Tag {
+  string key = 1;
+  TagValue value = 2;
+}
+
+message TagFamily {
+  string name = 1;
+  repeated Tag tags = 2;
+}
+
+// Condition consists of the query condition with a single binary operator to be imposed
+// For 1:1 BinaryOp, values in condition must be an array with length = 1,
+// while for 1:N BinaryOp, values can be an array with length >= 1.
+message Condition {
+  // BinaryOp specifies the operation imposed to the given query condition
+  // For EQ, NE, LT, GT, LE and GE, only one operand should be given, i.e. one-to-one relationship.
+  // HAVING and NOT_HAVING allow multi-value to be the operand such as array/vector, i.e. one-to-many relationship.
+  // For example, "keyA" contains "valueA" **and** "valueB"
+  // MATCH performances a full-text search if the tag is analyzed.
+  // The string value applies to the same analyzer as the tag, but string array value does not.
+  // Each item in a string array is seen as a token instead of a query expression.
+  enum BinaryOp {
+    BINARY_OP_UNSPECIFIED = 0;
+    BINARY_OP_EQ = 1;
+    BINARY_OP_NE = 2;
+    BINARY_OP_LT = 3;
+    BINARY_OP_GT = 4;
+    BINARY_OP_LE = 5;
+    BINARY_OP_GE = 6;
+    BINARY_OP_HAVING = 7;
+    BINARY_OP_NOT_HAVING = 8;
+    BINARY_OP_IN = 9;
+    BINARY_OP_NOT_IN = 10;
+    BINARY_OP_MATCH = 11;
+  }
+  string name = 1;
+  BinaryOp op = 2;
+  TagValue value = 3;
+  message MatchOption {
+    string analyzer = 1;
+    enum Operator {
+      OPERATOR_UNSPECIFIED = 0;
+      OPERATOR_AND = 1;
+      OPERATOR_OR = 2;
+    }
+    Operator operator = 2;
+  }
+  MatchOption match_option = 4;
+}
+
+// tag_families are indexed.
+message Criteria {
+  oneof exp {
+    LogicalExpression le = 1;
+    Condition condition = 2;
+  }
+}
+
+// LogicalExpression supports logical operation
+message LogicalExpression {
+  enum LogicalOp {
+    LOGICAL_OP_UNSPECIFIED = 0;
+    LOGICAL_OP_AND = 1;
+    LOGICAL_OP_OR = 2;
+  }
+  // op is a logical operation
+  LogicalOp op = 1;
+  Criteria left = 2;
+  Criteria right = 3;
+}
+
+enum Sort {
+  SORT_UNSPECIFIED = 0;
+  SORT_DESC = 1;
+  SORT_ASC = 2;
+}
+
+// QueryOrder means a Sort operation to be done for a given index rule.
+// The index_rule_name refers to the name of a index rule bound to the subject.
+message QueryOrder {
+  string index_rule_name = 1;
+  Sort sort = 2;
+}
+
+// TagProjection is used to select the names of keys to be returned.
+message TagProjection {
+  message TagFamily {
+    string name = 1;
+    repeated string tags = 2;
+  }
+  repeated TagFamily tag_families = 1;
+}
+
+// TimeRange is a range query for uint64,
+// the range here follows left-inclusive and right-exclusive rule, i.e. [begin, end) if both edges exist
+message TimeRange {
+  google.protobuf.Timestamp begin = 1;
+  google.protobuf.Timestamp end = 2;
+}
+
+// Status is the response status for write
+enum Status {
+  STATUS_UNSPECIFIED = 0;
+  STATUS_SUCCEED = 1;
+  STATUS_INVALID_TIMESTAMP = 2;
+  STATUS_NOT_FOUND = 3;
+  STATUS_EXPIRED_SCHEMA = 4;
+  STATUS_INTERNAL_ERROR = 5;
+  STATUS_DISK_FULL = 6;
+  STATUS_VERSION_UNSUPPORTED = 7; // Client version not supported
+  STATUS_VERSION_DEPRECATED = 8; // Client version deprecated but still supported
+  STATUS_METADATA_REQUIRED = 9; // Metadata is required for the first request
+}
diff --git a/proto/banyandb/v1/banyandb-property.proto b/proto/banyandb/v1/banyandb-property.proto
new file mode 100644
index 0000000..b602ef5
--- /dev/null
+++ b/proto/banyandb/v1/banyandb-property.proto
@@ -0,0 +1,112 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) 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.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.property.v1";
+
+package banyandb.property.v1;
+
+import "google/protobuf/timestamp.proto";
+import "validate/validate.proto";
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
+
+// Property stores the user defined data
+message Property {
+  // metadata is the identity of a property
+  common.v1.Metadata metadata = 1 [(validate.rules).message.required = true];
+  // id is the identity of a property
+  string id = 2 [(validate.rules).string.min_len = 1];
+  // tag stores the content of a property
+  repeated model.v1.Tag tags = 3 [(validate.rules).repeated.min_items = 1];
+  // updated_at indicates when the property is updated
+  google.protobuf.Timestamp updated_at = 4;
+}
+
+message ApplyRequest {
+  banyandb.property.v1.Property property = 1 [(validate.rules).message.required = true];
+  enum Strategy {
+    STRATEGY_UNSPECIFIED = 0;
+    STRATEGY_MERGE = 1;
+    STRATEGY_REPLACE = 2;
+  }
+  // strategy indicates how to update a property. It defaults to STRATEGY_MERGE
+  Strategy strategy = 2;
+}
+
+message ApplyResponse {
+  // created indicates whether the property existed.
+  // True: the property is absent. False: the property existed.
+  bool created = 1;
+  uint32 tags_num = 2;
+}
+
+message DeleteRequest {
+  // groups indicate where the data points are stored.
+  string group = 1 [(validate.rules).string.min_len = 1];
+  // name is the identity of a property.
+  string name = 2 [(validate.rules).string.min_len = 1];
+  // id is the identity of item in the property.
+  string id = 3;
+}
+
+message DeleteResponse {
+  bool deleted = 1;
+}
+
+message QueryOrder {
+  // tag_name is the name of the tag to be ordered.
+  string tag_name = 1;
+  // order_by is given to specify the sort for a tag.
+  model.v1.Sort sort = 12;
+}
+
+// QueryRequest is the request contract for query.
+message QueryRequest {
+  // groups indicate where the data points are stored.
+  repeated string groups = 1 [(validate.rules).repeated.min_items = 1];
+  // name is created when it receives the first property
+  string name = 2;
+  // ids is the identities of properties
+  repeated string ids = 3;
+  // criteria is used to filter properties based on tags
+  model.v1.Criteria criteria = 4;
+  // tag_projection can be used to select tags of the data points in the response
+  repeated string tag_projection = 5;
+  uint32 limit = 6;
+  // trace is used to enable trace for the query
+  bool trace = 7;
+  // order_by is given to specify the sort for a tag.
+  QueryOrder order_by = 8;
+}
+
+// QueryResponse is the response for a query to the Query module.
+message QueryResponse {
+  // properties are the actual data returned
+  repeated banyandb.property.v1.Property properties = 1;
+  // trace contains the trace information of the query when trace is enabled
+  common.v1.Trace trace = 2;
+}
+
+service PropertyService {
+  // Apply creates a property if it's absent, or update a existed one based on a strategy.
+  rpc Apply(ApplyRequest) returns (ApplyResponse);
+  rpc Delete(DeleteRequest) returns (DeleteResponse);
+
+  rpc Query(QueryRequest) returns (QueryResponse);
+}
diff --git a/proto/banyandb/v1/banyandb-stream.proto b/proto/banyandb/v1/banyandb-stream.proto
new file mode 100644
index 0000000..642b189
--- /dev/null
+++ b/proto/banyandb/v1/banyandb-stream.proto
@@ -0,0 +1,130 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) 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.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.stream.v1";
+
+package banyandb.stream.v1;
+
+import "google/protobuf/timestamp.proto";
+import "validate/validate.proto";
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
+
+// Element represents
+// (stream context) a Span defined in Google Dapper paper or equivalently a Segment in Skywalking.
+// (Log context) a log
+message Element {
+  // element_id could be span_id of a Span or segment_id of a Segment in the context of stream
+  string element_id = 1;
+  // timestamp represents a millisecond
+  // 1) either the start time of a Span/Segment,
+  // 2) or the timestamp of a log
+  google.protobuf.Timestamp timestamp = 2;
+  // fields contains all indexed Field. Some typical names,
+  // - stream_id
+  // - duration
+  // - service_name
+  // - service_instance_id
+  // - end_time_milliseconds
+  repeated model.v1.TagFamily tag_families = 3;
+}
+
+// QueryResponse is the response for a query to the Query module.
+message QueryResponse {
+  // elements are the actual data returned
+  repeated Element elements = 1;
+  // trace contains the trace information of the query when trace is enabled
+  common.v1.Trace trace = 2;
+}
+
+// QueryRequest is the request contract for query.
+message QueryRequest {
+  // groups indicate where the elements are stored.
+  repeated string groups = 1 [(validate.rules).repeated.min_items = 1];
+  // name is the identity of a stream.
+  string name = 2 [(validate.rules).string.min_len = 1];
+  // time_range is a range query with begin/end time of entities in the timeunit of milliseconds.
+  // In the context of stream, it represents the range of the `startTime` for spans/segments,
+  // while in the context of Log, it means the range of the timestamp(s) for logs.
+  // it is always recommended to specify time range for performance reason
+  model.v1.TimeRange time_range = 3;
+  // offset is used to support pagination, together with the following limit
+  uint32 offset = 4;
+  // limit is used to impose a boundary on the number of records being returned
+  uint32 limit = 5;
+  // order_by is given to specify the sort for a field. So far, only fields in the type of Integer are supported
+  model.v1.QueryOrder order_by = 6;
+  // tag_families are indexed.
+  model.v1.Criteria criteria = 7;
+  // projection can be used to select the key names of the element in the response
+  model.v1.TagProjection projection = 8 [(validate.rules).message.required = true];
+  // trace is used to enable trace for the query
+  bool trace = 9;
+  // stage is used to specify the stage of the query in the lifecycle
+  repeated string stages = 10;
+}
+
+
+
+service StreamService {
+  rpc Query(QueryRequest) returns (QueryResponse);
+  rpc Write(stream WriteRequest) returns (stream WriteResponse);
+
+}
+
+// TagFamilySpec defines the specification of a tag family.
+message TagFamilySpec {
+  // name of the tag family
+  string name = 1;
+  // names of tags in the tag family
+  repeated string tag_names = 2;
+}
+
+message ElementValue {
+  // element_id could be span_id of a Span or segment_id of a Segment in the context of stream
+  string element_id = 1;
+  // timestamp is in the timeunit of milliseconds. It represents
+  // 1) either the start time of a Span/Segment,
+  // 2) or the timestamp of a log
+  google.protobuf.Timestamp timestamp = 2;
+  // the order of tag_families' items match TagFamilySpec
+  repeated model.v1.TagFamilyForWrite tag_families = 3;
+}
+
+message WriteRequest {
+  // the metadata is required only for the first request of gRPC stream.
+  common.v1.Metadata metadata = 1;
+  // the element is required.
+  ElementValue element = 2 [(validate.rules).message.required = true];
+  // the message_id is required.
+  uint64 message_id = 3 [(validate.rules).uint64.gt = 0];
+  // the tag family specification.
+  // If this is not set with the indicated metadata, use the schema definition.
+  // If this is not set, use the existing spec declaration from previous requests in the current gRPC stream.
+  repeated TagFamilySpec tag_family_spec = 4;
+}
+
+message WriteResponse {
+  // the message_id from request.
+  uint64 message_id = 1 [(validate.rules).uint64.gt = 0];
+  // status indicates the request processing result
+  string status = 2;
+  // the metadata from request when request fails
+  common.v1.Metadata metadata = 3;
+}
diff --git a/proto/banyandb/v1/banyandb-trace.proto b/proto/banyandb/v1/banyandb-trace.proto
new file mode 100644
index 0000000..b822b59
--- /dev/null
+++ b/proto/banyandb/v1/banyandb-trace.proto
@@ -0,0 +1,122 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) 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.
+
+syntax = "proto3";
+
+option java_package = "org.apache.skywalking.banyandb.trace.v1";
+
+package banyandb.trace.v1;
+
+import "validate/validate.proto";
+import "banyandb/v1/banyandb-common.proto";
+import "banyandb/v1/banyandb-model.proto";
+
+// Span is a single operation within a trace.
+message Span {
+  // tags are the indexed tags of the span.
+  repeated model.v1.Tag tags = 1;
+  // span is the raw span data.
+  bytes span = 2;
+  // span_id is the unique identifier of the span.
+  string span_id = 3;
+}
+
+// InternalTrace is the trace that is used for internal use.
+message InternalTrace {
+  // spans are the spans that belong to this trace.
+  repeated Span spans = 1;
+  // trace_id is the unique identifier of the trace.
+  string trace_id = 2;
+  // key is used for sorting.
+  int64 key = 3;
+}
+
+// Trace contains all spans that belong to a single trace ID.
+message Trace {
+  // spans is the list of spans that belong to this trace.
+  repeated Span spans = 1;
+  // trace_id is the unique identifier of the trace.
+  string trace_id = 2;
+}
+
+// QueryResponse is the response of a query.
+message QueryResponse {
+  // traces is a list of traces that match the query, with spans grouped by trace ID.
+  repeated Trace traces = 1;
+  // trace_query_result contains the trace of the query execution if tracing is enabled.
+  common.v1.Trace trace_query_result = 2;
+}
+
+// InternalQueryResponse is the response of an internal query.
+message InternalQueryResponse {
+  // internal_traces is a list of internal traces that match the query.
+  repeated InternalTrace internal_traces = 1;
+  // trace_query_result contains the trace of the query execution if tracing is enabled.
+  common.v1.Trace trace_query_result = 2;
+}
+
+// QueryRequest is the request contract for query.
+message QueryRequest {
+  // groups indicates the physical data location.
+  repeated string groups = 1 [(validate.rules).repeated.min_items = 1];
+  // name is the identity of a trace.
+  string name = 2 [(validate.rules).string.min_len = 1];
+  // time_range is a range query with begin/end time of entities in the timeunit of milliseconds.
+  // In the context of trace, it represents the range of the `startTime` for spans/segments,
+  // it is always recommended to specify time range for performance reason
+  model.v1.TimeRange time_range = 3;
+  // offset is used to support pagination, together with the following limit
+  uint32 offset = 4;
+  // limit is used to impose a boundary on the number of spans being returned
+  uint32 limit = 5;
+  // order_by is given to specify the sort for a tag. So far, only tags in the type of Integer are supported
+  model.v1.QueryOrder order_by = 6;
+  // criteria is the filter criteria.
+  model.v1.Criteria criteria = 7;
+  // projection can be used to select the names of the tags in the response
+  repeated string tag_projection = 8;
+  // trace is used to enable trace for the query
+  bool trace = 9;
+  // stage is used to specify the stage of the query in the lifecycle
+  repeated string stages = 10;
+}
+
+
+
+service TraceService {
+  rpc Query(QueryRequest) returns (QueryResponse);
+  rpc Write(stream WriteRequest) returns (stream WriteResponse);
+
+}
+
+message TagSpec {
+  repeated string tag_names = 1;
+}
+
+message WriteRequest {
+  common.v1.Metadata metadata = 1;
+  repeated model.v1.TagValue tags = 2 [(validate.rules).repeated.min_items = 1];
+  bytes span = 3;
+  uint64 version = 4 [(validate.rules).uint64.gt = 0];
+  TagSpec tag_spec = 5;
+}
+
+message WriteResponse {
+  common.v1.Metadata metadata = 1;
+  uint64 version = 2;
+  string status = 3;
+}