diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..38bece4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index dab2f7c..d441fcd 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Fetch submodules run: git submodule update --init --recursive - name: Restore dependencies diff --git a/Mocha.sln b/Mocha.sln index 3b2cac6..7acd0a3 100644 --- a/Mocha.sln +++ b/Mocha.sln @@ -16,12 +16,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Core.Tests", "tests\M EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{71D20701-E657-443F-8000-7958FB5E6BD7}" ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore .editorconfig = .editorconfig .gitignore = .gitignore .gitmodules = .gitmodules LICENSE = LICENSE README.md = README.md .github\workflows\dotnet-build.yml = .github\workflows\dotnet-build.yml + README.zh-CN.md = README.zh-CN.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Storage", "src\Mocha.Storage\Mocha.Storage.csproj", "{8EEB6697-B975-430D-9CC3-3048E76C5ECA}" @@ -30,6 +32,30 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Core.Benchmarks", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Storage.Tests", "tests\Mocha.Storage.Tests\Mocha.Storage.Tests.csproj", "{FC0D810E-4ACC-4567-95D8-D7F617E412FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mocha.Query.Jaeger", "src\Mocha.Query.Jaeger\Mocha.Query.Jaeger.csproj", "{DC281C3B-455F-4391-92EF-D5D99FC2B9AA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{53AF2923-4CB8-44C8-885B-B0EEB8574FEB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "mysql", "mysql", "{1DCDFC33-1401-4CCA-AAAE-FC150AD147F5}" + ProjectSection(SolutionItems) = preProject + scripts\mysql\init.sql = scripts\mysql\init.sql + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{D598862A-999C-40FD-A190-EBD00376D077}" + ProjectSection(SolutionItems) = preProject + docker\docker-compose.yml = docker\docker-compose.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "distributor", "distributor", "{959DCB4E-A070-4C66-A27F-D83CB933F0D8}" + ProjectSection(SolutionItems) = preProject + docker\distributor\Dockerfile = docker\distributor\Dockerfile + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "jaeger-query", "jaeger-query", "{C7222A9C-C50C-4FF0-A02D-778A9BB4DD2C}" + ProjectSection(SolutionItems) = preProject + docker\jaeger-query\Dockerfile = docker\jaeger-query\Dockerfile + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -44,6 +70,10 @@ Global {8EEB6697-B975-430D-9CC3-3048E76C5ECA} = {6983D239-07DA-4DFA-9AAA-F6876029FF8D} {2107E75D-9717-4CCD-BE85-713BEF75366A} = {24F9E34A-D92A-4C0A-851F-1E864181BF97} {FC0D810E-4ACC-4567-95D8-D7F617E412FE} = {24F9E34A-D92A-4C0A-851F-1E864181BF97} + {DC281C3B-455F-4391-92EF-D5D99FC2B9AA} = {6983D239-07DA-4DFA-9AAA-F6876029FF8D} + {1DCDFC33-1401-4CCA-AAAE-FC150AD147F5} = {53AF2923-4CB8-44C8-885B-B0EEB8574FEB} + {959DCB4E-A070-4C66-A27F-D83CB933F0D8} = {D598862A-999C-40FD-A190-EBD00376D077} + {C7222A9C-C50C-4FF0-A02D-778A9BB4DD2C} = {D598862A-999C-40FD-A190-EBD00376D077} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DCA600F0-4D6C-44DA-A493-F63097CCE74E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -78,5 +108,9 @@ Global {FC0D810E-4ACC-4567-95D8-D7F617E412FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC0D810E-4ACC-4567-95D8-D7F617E412FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC0D810E-4ACC-4567-95D8-D7F617E412FE}.Release|Any CPU.Build.0 = Release|Any CPU + {DC281C3B-455F-4391-92EF-D5D99FC2B9AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC281C3B-455F-4391-92EF-D5D99FC2B9AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC281C3B-455F-4391-92EF-D5D99FC2B9AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC281C3B-455F-4391-92EF-D5D99FC2B9AA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/README.md b/README.md index a4c22d7..a720f9c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,18 @@ -# mocha +Mocha +===== + +[![codecov](https://codecov.io/gh/dotnetcore/mocha/branch/main/graph/badge.svg?token=v9OE7dV8ZS)](https://codecov.io/gh/dotnetcore/mocha) + +English | [简体中文](./README.zh-CN.md) + Mocha is an application performance monitor tools based on [OpenTelemetry](https://opentelemetry.io), which also provides a scalable platform for observability data analysis and storage. -# functional architecture +## Quick Start +In the beta phase, we provide a Docker Compose file for users to experience our system locally. + ++ [Quick Start](./docs/quick-start/docker-compose/quick-start.en-US.md) + +## Functional Architecture ![](./docs/assets/functional_architecture.png) The set of features that Mocha will provide: @@ -21,7 +32,7 @@ The set of features that Mocha will provide: - Alert notifications - Metrics/Logs/Traces data explore -# technical architecture +## Technical Architecture ![](./docs/assets/technical_architecture.png) The components of Mocha are as follows: @@ -32,8 +43,8 @@ The components of Mocha are as follows: - Mocha Manager : Consisting of a manager server, dashboard, and ETCD for cluster metadata and data analysis rules storage. - OTel SDK / Collector : Open-source OpenTelemetry collection kits -# contribute +## Contribute One of the easiest ways to contribute is to participate in discussions and discuss issues. You can also contribute by submitting pull requests with code changes. -# license -Mocha is under the MIT license. See the [LICENSE](LICENSE) file for details. \ No newline at end of file +## License +Mocha is under the MIT license. See the [LICENSE](LICENSE) file for details. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..2288c4e --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,50 @@ +Mocha +===== + +[![codecov](https://codecov.io/gh/dotnetcore/mocha/branch/main/graph/badge.svg?token=v9OE7dV8ZS)](https://codecov.io/gh/dotnetcore/mocha) + +[English](./README.md) | 简体中文 + +Mocha 是一个基于 [OpenTelemetry](https://opentelemetry.io) 的 APM 系统,同时提供可伸缩的可观测性数据分析和存储平台。 + +## 快速开始 +现阶段,我们提供了 Docker Compose 文件,方便用户在本地体验我们的系统。 + ++ [快速开始](./docs/quick-start/docker-compose/quick-start.zh-CN.md) + +# 平台功能 +![](./docs/assets/functional_architecture.png) +Mocha 将要提供的功能集合: +- APM 和 分布式追踪 + - 服务概览、R.E.D 指标和可用性监控 + - 服务拓扑 + - 调用监控,包括 HTTP、RPC、Cache、DB、MQ 等 + - 调用链路查询和检索 +- 基础设施监控 + - 主机监控 + - 容器和 Kubernetes 监控 + - 主流中间件监控 +- 日志 + - 日志查询 + - 日志聚合分析 +- 报警 + - 报警规则管理 + - 报警通知 +- M.T.L 数据探索 [Data Explore / Inspect] + +# 技术架构 +![](./docs/assets/technical_architecture.png) + +Mocha 整体架构由下面的部分组成 +- Mocha Distributor Cluster:作为 mocha 系统的数据入口,负责接收 OTel SDK 和 Collector 上报的数据,并通过一致性Hash 将数据路由到对应的 aggregator 节点上。为了保证数据不丢失,最终 Distributor 应该具备本地 FIFO 队列的能力。 +- Mocha Streaming Cluster:mocha 的核心组件,通过读取预配置或者用户配置的 aggr rule dsl 生成对应的 streaming data flow 并执行。Streaming 是具备分布式 shuffle 的能力的有状态组件,需要将自身信息注册到ETCD中。 +- Storage:mocha M.T.L 存储,可以选用开源存储组件,如 ClickHouse、ElasticSearch、victoriametrics 等。 +- Mocha Querier + Grafana: 从存储查询数据并提供给 grafana 做展示。因此要兼容 promql / jeager / loki 等数据源的查询。 +- Mocha Manager : 包括 manager server、dashboard和ETCE组件,集群元数据和 M.T.L 数据分析规则存储。 +- OTel SDK / Collector : 开源 OpenTelemetry 采集套件。 + +## 参与贡献 +参与贡献的最简单的方式是参与讨论并讨论问题。您也可以通过提交代码更改的拉取请求来进行贡献。 + +## 许可证 +Mocha 是在 MIT 许可下发布的。有关详细信息,请参阅 [LICENSE](LICENSE) 文件。 diff --git a/docker/distributor/Dockerfile b/docker/distributor/Dockerfile new file mode 100644 index 0000000..f6ea57c --- /dev/null +++ b/docker/distributor/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 4319 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["src/Mocha.Distributor/Mocha.Distributor.csproj", "src/Mocha.Distributor/"] +COPY ["src/Mocha.Core/Mocha.Core.csproj", "src/Mocha.Core/"] +COPY ["src/Mocha.Protocol.Generated/Mocha.Protocol.Generated.csproj", "src/Mocha.Protocol.Generated/"] +COPY ["src/Mocha.Storage/Mocha.Storage.csproj", "src/Mocha.Storage/"] +RUN dotnet restore "src/Mocha.Distributor/Mocha.Distributor.csproj" +COPY . . +WORKDIR "/src/src/Mocha.Distributor" +RUN dotnet build "Mocha.Distributor.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Mocha.Distributor.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Mocha.Distributor.dll"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..70dba7f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,81 @@ +# Licensed to the .NET Core Community under one or more agreements. +# The .NET Core Community licenses this file to you under the MIT license. + +version: "3.8" + +name: mocha + +services: + mysql: + image: mysql:8.2.0 + container_name: mocha-mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: mocha + MYSQL_USER: mocha + MYSQL_PASSWORD: mocha + ports: + - "3306:3306" + expose: + - "3306" + volumes: + - ../scripts/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql +# - ./mysql:/var/lib/mysql + restart: always + networks: + - mocha + + grafana: + image: grafana/grafana:10.0.10 + container_name: mocha-grafana + ports: + - "3000:3000" +# volumes: +# - ./grafana:/var/lib/grafana + restart: always + networks: + - mocha + + distributor: + build: + context: .. + dockerfile: ./docker/distributor/Dockerfile + container_name: mocha-distributor + ports: + - "4317:4317" + expose: + - "4317" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ConnectionStrings__EF=server=mysql;port=3306;database=mocha;userid=root;password=root + - Logging__LogLevel__Microsoft.EntityFrameworkCore=Warning + depends_on: + - mysql + restart: always + networks: + - mocha + + jaeger-query: + build: + context: .. + dockerfile: ./docker/jaeger-query/Dockerfile + container_name: mocha-jaeger-query + ports: + - "5775:5775" + expose: + - "5775" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ConnectionStrings__EF=server=mysql;port=3306;database=mocha;userid=mocha;password=mocha + - Logging__LogLevel__Microsoft.EntityFrameworkCore=Warning + depends_on: + - mysql + restart: always + networks: + - mocha + +networks: + mocha: + driver: bridge + + diff --git a/docker/jaeger-query/Dockerfile b/docker/jaeger-query/Dockerfile new file mode 100644 index 0000000..38596d1 --- /dev/null +++ b/docker/jaeger-query/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["src/Mocha.Query.Jaeger/Mocha.Query.Jaeger.csproj", "src/Mocha.Query.Jaeger/"] +COPY ["src/Mocha.Core/Mocha.Core.csproj", "src/Mocha.Core/"] +COPY ["src/Mocha.Protocol.Generated/Mocha.Protocol.Generated.csproj", "src/Mocha.Protocol.Generated/"] +COPY ["src/Mocha.Storage/Mocha.Storage.csproj", "src/Mocha.Storage/"] +RUN dotnet restore "src/Mocha.Query.Jaeger/Mocha.Query.Jaeger.csproj" +COPY . . +WORKDIR "/src/src/Mocha.Query.Jaeger" +RUN dotnet build "Mocha.Query.Jaeger.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Mocha.Query.Jaeger.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Mocha.Query.Jaeger.dll"] diff --git a/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-2.png b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-2.png new file mode 100644 index 0000000..c33660d Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-2.png differ diff --git a/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-3.png b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-3.png new file mode 100644 index 0000000..17a30b3 Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-3.png differ diff --git a/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-4.png b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-4.png new file mode 100644 index 0000000..3c10bca Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-4.png differ diff --git a/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-5.png b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-5.png new file mode 100644 index 0000000..eba31e5 Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-5.png differ diff --git a/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-warning.png b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-warning.png new file mode 100644 index 0000000..ee37d7a Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source-warning.png differ diff --git a/docs/quick-start/docker-compose/asserts/add-jaeger-data-source.png b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source.png new file mode 100644 index 0000000..7534560 Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/add-jaeger-data-source.png differ diff --git a/docs/quick-start/docker-compose/asserts/query-trace-2.png b/docs/quick-start/docker-compose/asserts/query-trace-2.png new file mode 100644 index 0000000..ef7fa6e Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/query-trace-2.png differ diff --git a/docs/quick-start/docker-compose/asserts/query-trace.png b/docs/quick-start/docker-compose/asserts/query-trace.png new file mode 100644 index 0000000..4d7b080 Binary files /dev/null and b/docs/quick-start/docker-compose/asserts/query-trace.png differ diff --git a/docs/quick-start/docker-compose/quick-start.en-US.md b/docs/quick-start/docker-compose/quick-start.en-US.md new file mode 100644 index 0000000..a2cc02b --- /dev/null +++ b/docs/quick-start/docker-compose/quick-start.en-US.md @@ -0,0 +1,53 @@ +## Start the Project + +Execute the following command in the docker directory under the project root directory to start the project: + +```bash +docker-compose up -d +``` + +After the startup is successful, you can see the following containers: + ++ distributor: Provides gRPC API for receiving OTLP data ++ jaeger-query: Provides HTTP API for receiving Jaeger query protocol ++ mysql: Used to store data ++ grafana: Used to display data + +## Send Trace Data + +Configure the OTLP exporter of the SDK as `http://localhost:4317` to send data to the distributor. + +## Configure Jaeger Data Source + +We have implemented an API that supports the Jaeger query protocol, so you can configure the Jaeger data source directly in Grafana. + +Visit http://localhost:3000/ to see the grafana login page. Both the username and password are admin. + +After logging in, click the menu on the left, select Data Sources, and then click Add data source. + +![](./asserts/add-jaeger-data-source.png) + +![](./asserts/add-jaeger-data-source-2.png) + +Select Jaeger. +![](./asserts/add-jaeger-data-source-3.png) + +Configure the URL of the Jaeger data source as `http://jaeger-query:5775`. + +![](./asserts/add-jaeger-data-source-4.png) + +Click Save & Test. If the following information is displayed, the configuration is successful. + +![](./asserts/add-jaeger-data-source-5.png) + +If no data has been sent to the distributor yet, the following warning message will be displayed. + +![](./asserts/add-jaeger-data-source-warning.png) + +## Query Trace Data + +Click the menu on the left, select Explore, and then select the Jaeger data source to see the Trace data. + +![](./asserts/query-trace.png) + +![](./asserts/query-trace-2.png) \ No newline at end of file diff --git a/docs/quick-start/docker-compose/quick-start.zh-CN.md b/docs/quick-start/docker-compose/quick-start.zh-CN.md new file mode 100644 index 0000000..769db3d --- /dev/null +++ b/docs/quick-start/docker-compose/quick-start.zh-CN.md @@ -0,0 +1,49 @@ +## 启动项目 + +在项目根目录下的docker目录中,执行以下命令启动项目: + +```bash +docker-compose up -d +``` + +启动成功后,可以看到以下容器: + ++ distributor: 提供用于接收 OTLP 数据的 gRPC API ++ jaeger-query: 提供用于接收 Jaeger 查询协议的 HTTP API ++ mysql: 用于存储数据 ++ grafana: 用于展示数据 + +## Trace 数据的发送 + +将 SDK 的 OTLP exporter 配置为 `http://localhost:4317` 即可将数据发送到 distributor。 + +## 配置 Jaeger 数据源 + +我们实现了支持 Jaeger 查询协议的 API,因此可以直接在 Grafana 中配置 Jaeger 数据源。 + +访问 http://localhost:3000/ 即可看到grafana的登录页面。用户名和密码都是admin。 + +登录后,点击左侧的菜单,选择 Data Sources,然后点击 Add data source。 + +![](./asserts/add-jaeger-data-source.png) + +![](./asserts/add-jaeger-data-source-2.png) + +选择 Jaeger。 +![](./asserts/add-jaeger-data-source-3.png) + +配置 Jaeger 数据源的 URL 为 `http://jaeger-query:5775`。 +![](./asserts/add-jaeger-data-source-4.png) + +点击 Save & Test,如果显示如下信息,则说明配置成功。 +![](./asserts/add-jaeger-data-source-5.png) + +如果还没往 distributor 发送过数据,会显示如下警告信息。 +![](./asserts/add-jaeger-data-source-warning.png) + +## Trace 数据的查询 + +点击左侧的菜单,选择 Explore,然后选择 Jaeger 数据源,即可看到 Trace 数据。 +![](./asserts/query-trace.png) + +![](./asserts/query-trace-2.png) \ No newline at end of file diff --git a/scripts/mysql/init.sql b/scripts/mysql/init.sql new file mode 100644 index 0000000..42c9569 --- /dev/null +++ b/scripts/mysql/init.sql @@ -0,0 +1,98 @@ +-- Licensed to the .NET Core Community under one or more agreements. +-- The .NET Core Community licenses this file to you under the MIT license. + +CREATE DATABASE IF NOT EXISTS mocha DEFAULT CHARACTER SET utf8mb4; + +USE mocha; + +CREATE TABLE IF NOT EXISTS span +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + trace_id VARCHAR(255) NOT NULL, + span_id VARCHAR(255) NOT NULL, + span_name VARCHAR(255) NOT NULL, + parent_span_id VARCHAR(255), + start_time_unix_nano BIGINT UNSIGNED NOT NULL, + end_time_unix_nano BIGINT UNSIGNED NOT NULL, + duration_nanoseconds BIGINT UNSIGNED NOT NULL, + status_code INT, + status_message VARCHAR(1024), + span_kind int NOT NULL, + service_name VARCHAR(255) NOT NULL, + service_instance_id VARCHAR(255) NOT NULL, + trace_flags INT UNSIGNED NOT NULL, + trace_state VARCHAR(1024), + INDEX idx_trace_id (trace_id), + INDEX idx_span_id (span_id) +); + +CREATE TABLE IF NOT EXISTS span_attribute +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + trace_id VARCHAR(255) NOT NULL, + span_id VARCHAR(255) NOT NULL, + `key` VARCHAR(255) NOT NULL, + value_type int NOT NULL, + `value` VARCHAR(255) NOT NULL, + INDEX idx_trace_id (trace_id), + INDEX idx_span_id (span_id), + INDEX idx_key_value (`key`, `value`) +); + +CREATE TABLE IF NOT EXISTS resource_attribute +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + trace_id VARCHAR(255) NOT NULL, + span_id VARCHAR(255) NOT NULL, + `key` VARCHAR(255) NOT NULL, + value_type int NOT NULL, + `value` VARCHAR(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS span_event +( + Id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + trace_id VARCHAR(255) NOT NULL, + span_id VARCHAR(255) NOT NULL, + `index` int NOT NULL, + name VARCHAR(255) NOT NULL, + timestamp_unix_nano BIGINT UNSIGNED NOT NULL, + INDEX idx_trace_id (trace_id), + INDEX idx_span_id (span_id) +); + +CREATE TABLE IF NOT EXISTS span_event_attribute +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + trace_id VARCHAR(255) NOT NULL, + span_event_index int NOT NULL, + span_id VARCHAR(255) NOT NULL, + `key` VARCHAR(255) NOT NULL, + value_type int NOT NULL, + `value` VARCHAR(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS span_link +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + trace_id VARCHAR(255) NOT NULL, + span_id VARCHAR(255) NOT NULL, + `index` int NOT NULL, + linked_trace_id VARCHAR(255) NOT NULL, + linked_span_id VARCHAR(255) NOT NULL, + linked_trace_state VARCHAR(1024), + linked_trace_flags INT UNSIGNED, + INDEX idx_trace_id (trace_id), + INDEX idx_span_id (span_id) +); + +CREATE TABLE IF NOT EXISTS span_link_attribute +( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + trace_id VARCHAR(255) NOT NULL, + span_link_index int NOT NULL, + span_id VARCHAR(255) NOT NULL, + `key` VARCHAR(255) NOT NULL, + value_type int NOT NULL, + `value` VARCHAR(255) NOT NULL +); diff --git a/src/Mocha.Core/Buffer/BufferConsumerOptions.cs b/src/Mocha.Core/Buffer/BufferConsumerOptions.cs index a3fad4a..b00b81d 100644 --- a/src/Mocha.Core/Buffer/BufferConsumerOptions.cs +++ b/src/Mocha.Core/Buffer/BufferConsumerOptions.cs @@ -10,4 +10,6 @@ public class BufferConsumerOptions public string GroupName { get; init; } = default!; public bool AutoCommit { get; init; } + + public int BatchSize { get; init; } = 100; } diff --git a/src/Mocha.Core/Buffer/BufferOptionsBuilder.cs b/src/Mocha.Core/Buffer/BufferOptionsBuilder.cs index 4f0b3fe..b0e5e9d 100644 --- a/src/Mocha.Core/Buffer/BufferOptionsBuilder.cs +++ b/src/Mocha.Core/Buffer/BufferOptionsBuilder.cs @@ -5,12 +5,7 @@ namespace Mocha.Core.Buffer; -public class BufferOptionsBuilder +public class BufferOptionsBuilder(IServiceCollection services) { - public BufferOptionsBuilder(IServiceCollection services) - { - Services = services; - } - - public IServiceCollection Services { get; } + public IServiceCollection Services { get; } = services; } diff --git a/src/Mocha.Core/Buffer/BufferQueue.cs b/src/Mocha.Core/Buffer/BufferQueue.cs index 676a246..1d71861 100644 --- a/src/Mocha.Core/Buffer/BufferQueue.cs +++ b/src/Mocha.Core/Buffer/BufferQueue.cs @@ -5,33 +5,26 @@ namespace Mocha.Core.Buffer; -internal class BufferQueue : IBufferQueue +internal class BufferQueue(IServiceProvider serviceProvider) : IBufferQueue { - private readonly IServiceProvider _serviceProvider; - - public BufferQueue(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - public IBufferProducer CreateProducer(string topicName) { ArgumentException.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - var queue = _serviceProvider.GetRequiredKeyedService>(topicName); + var queue = serviceProvider.GetRequiredKeyedService>(topicName); return queue.CreateProducer(); } public IBufferConsumer CreateConsumer(BufferConsumerOptions options) { ArgumentException.ThrowIfNullOrEmpty(options.TopicName, nameof(options.TopicName)); - var queue = _serviceProvider.GetRequiredKeyedService>(options.TopicName); + var queue = serviceProvider.GetRequiredKeyedService>(options.TopicName); return queue.CreateConsumer(options); } public IEnumerable> CreateConsumers(BufferConsumerOptions options, int consumerNumber) { ArgumentException.ThrowIfNullOrEmpty(options.TopicName, nameof(options.TopicName)); - var queue = _serviceProvider.GetRequiredKeyedService>(options.TopicName); + var queue = serviceProvider.GetRequiredKeyedService>(options.TopicName); return queue.CreateConsumers(options, consumerNumber); } } diff --git a/src/Mocha.Core/Buffer/IBufferConsumer.cs b/src/Mocha.Core/Buffer/IBufferConsumer.cs index c1388f9..d0f2e4c 100644 --- a/src/Mocha.Core/Buffer/IBufferConsumer.cs +++ b/src/Mocha.Core/Buffer/IBufferConsumer.cs @@ -9,7 +9,7 @@ public interface IBufferConsumer string GroupName { get; } - IAsyncEnumerable ConsumeAsync(CancellationToken cancellationToken = default); + IAsyncEnumerable> ConsumeAsync(CancellationToken cancellationToken = default); ValueTask CommitAsync(); } diff --git a/src/Mocha.Core/Buffer/Memory/MemoryBufferConsumer.cs b/src/Mocha.Core/Buffer/Memory/MemoryBufferConsumer.cs index e750644..472e1d3 100644 --- a/src/Mocha.Core/Buffer/Memory/MemoryBufferConsumer.cs +++ b/src/Mocha.Core/Buffer/Memory/MemoryBufferConsumer.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Core Community under one or more agreements. // The .NET Core Community licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Mocha.Core.Buffer.Memory; @@ -39,7 +40,7 @@ public void AssignPartitions(params MemoryBufferPartition[] partitions) } } - public async IAsyncEnumerable ConsumeAsync( + public async IAsyncEnumerable> ConsumeAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (_assignedPartitions.Length == 0) @@ -53,14 +54,16 @@ public async IAsyncEnumerable ConsumeAsync( var partition = SelectPartition(); - if (TryPull(partition, out var item)) + var batchSize = _options.BatchSize; + + if (TryPull(partition, batchSize, out var items)) { - yield return item; + yield return items; continue; } // Try to pull from other partitions - T itemFromOtherPartition = default!; + IEnumerable itemsFromOtherPartition = default!; var hasItemFromOtherPartition = false; foreach (var t in _assignedPartitions) @@ -72,9 +75,9 @@ public async IAsyncEnumerable ConsumeAsync( continue; } - if (TryPull(partition, out item)) + if (TryPull(partition, batchSize, out items)) { - itemFromOtherPartition = item; + itemsFromOtherPartition = items; hasItemFromOtherPartition = true; break; } @@ -82,7 +85,7 @@ public async IAsyncEnumerable ConsumeAsync( if (hasItemFromOtherPartition) { - yield return itemFromOtherPartition; + yield return itemsFromOtherPartition; continue; } @@ -111,9 +114,9 @@ public async IAsyncEnumerable ConsumeAsync( ? pendingDataTask.Result : await pendingDataTask; - if (TryPull(partitionWithNewData, out item)) + if (TryPull(partitionWithNewData, batchSize, out items)) { - yield return item; + yield return items; } } } @@ -168,10 +171,11 @@ public void NotifyNewDataAvailable(MemoryBufferPartition partition) } } - private bool TryPull(MemoryBufferPartition partition, out T item) + private bool TryPull(MemoryBufferPartition partition, int batchSize, + [NotNullWhen(true)] out IEnumerable? items) { _partitionBeingConsumed = partition; - var dataAvailable = partition.TryPull(_options.GroupName, out item); + var dataAvailable = partition.TryPull(_options.GroupName, batchSize, out items); if (dataAvailable) { diff --git a/src/Mocha.Core/Buffer/Memory/MemoryBufferOptions.cs b/src/Mocha.Core/Buffer/Memory/MemoryBufferOptions.cs index f1ed839..f289982 100644 --- a/src/Mocha.Core/Buffer/Memory/MemoryBufferOptions.cs +++ b/src/Mocha.Core/Buffer/Memory/MemoryBufferOptions.cs @@ -5,18 +5,11 @@ namespace Mocha.Core.Buffer.Memory; -public class MemoryBufferOptions +public class MemoryBufferOptions(IServiceCollection services) { - private readonly IServiceCollection _services; - - public MemoryBufferOptions(IServiceCollection services) - { - _services = services; - } - public MemoryBufferOptions AddTopic(string topicName, int partitionNumber) { - _services.AddKeyedSingleton>(topicName, new MemoryBufferQueue(topicName, partitionNumber)); + services.AddKeyedSingleton>(topicName, new MemoryBufferQueue(topicName, partitionNumber)); return this; } } diff --git a/src/Mocha.Core/Buffer/Memory/MemoryBufferPartition.cs b/src/Mocha.Core/Buffer/Memory/MemoryBufferPartition.cs index 43ae864..8141ee3 100644 --- a/src/Mocha.Core/Buffer/Memory/MemoryBufferPartition.cs +++ b/src/Mocha.Core/Buffer/Memory/MemoryBufferPartition.cs @@ -84,13 +84,13 @@ public void Enqueue(T item) } } - public bool TryPull(string groupName, [NotNullWhen(true)] out T item) + public bool TryPull(string groupName, int batchSize, [NotNullWhen(true)] out IEnumerable? items) { var reader = _consumerReaders.GetOrAdd( groupName, _ => new Reader(_head, _head.StartOffset)); - return reader.TryRead(out item); + return reader.TryRead(batchSize, out items); } public void Commit(string groupName) @@ -166,6 +166,7 @@ private sealed class Reader { private MemoryBufferSegment _currentSegment; private MemoryBufferPartitionOffset _pendingOffset; + private int _lastReadCount; public Reader(MemoryBufferSegment currentSegment, MemoryBufferPartitionOffset currentOffset) { @@ -175,26 +176,73 @@ public Reader(MemoryBufferSegment currentSegment, MemoryBufferPartitionOffset public MemoryBufferPartitionOffset PendingOffset => _pendingOffset; - public bool TryRead(out T item) + public bool TryRead(int batchSize, [NotNullWhen(true)] out IEnumerable? items) { - var segment = SelectSegment(); - return segment.TryGet(_pendingOffset, out item); - } + var remainingCount = batchSize; + var pendingOffset = _pendingOffset; + var result = Enumerable.Empty(); + var currentSegment = _currentSegment; - public void MoveNext() => _pendingOffset++; + while (true) + { + if (currentSegment.EndOffset < pendingOffset) + { + if (currentSegment.NextSegment == null) + { + break; + } - private MemoryBufferSegment SelectSegment() - { - var currentSegment = _currentSegment; - var nextSegment = currentSegment.NextSegment; - var moveToNextSegment = currentSegment.EndOffset < _pendingOffset && nextSegment != null; + currentSegment = currentSegment.NextSegment; + } + + var retrievalSuccess = currentSegment.TryGet(pendingOffset, remainingCount, out var segmentItems); + if (retrievalSuccess) + { + var length = segmentItems!.Length; + pendingOffset += (ulong)length; + remainingCount -= length; + result = result.Concat(segmentItems); + } + else + { + break; + } + + if (remainingCount == 0) + { + break; + } - if (moveToNextSegment) + var nextSegment = currentSegment.NextSegment; + var continueReading = nextSegment != null; + if (continueReading) + { + currentSegment = nextSegment!; + } + else + { + break; + } + } + + if (remainingCount == batchSize) { - _currentSegment = nextSegment!; + items = null; + return false; } - return _currentSegment; + _lastReadCount = batchSize - remainingCount; + items = result; + return true; + } + + public void MoveNext() + { + _pendingOffset += (ulong)_lastReadCount; + while (_currentSegment.EndOffset < _pendingOffset && _currentSegment.NextSegment != null) + { + _currentSegment = _currentSegment.NextSegment!; + } } } diff --git a/src/Mocha.Core/Buffer/Memory/MemoryBufferPartitionOffset.cs b/src/Mocha.Core/Buffer/Memory/MemoryBufferPartitionOffset.cs index 7b7ee49..2cb1ec2 100644 --- a/src/Mocha.Core/Buffer/Memory/MemoryBufferPartitionOffset.cs +++ b/src/Mocha.Core/Buffer/Memory/MemoryBufferPartitionOffset.cs @@ -15,8 +15,20 @@ public ulong ToUInt64() throw new OverflowException("Offset is too large to be converted to UInt64."); } + public int ToInt32() + { + if (Generation == 0 && Index <= int.MaxValue) + { + return (int)Index; + } + + throw new OverflowException("Offset is too large to be converted to Int32."); + } + public static explicit operator ulong(MemoryBufferPartitionOffset offset) => offset.ToUInt64(); + public static explicit operator int(MemoryBufferPartitionOffset offset) => offset.ToInt32(); + public static bool operator >(MemoryBufferPartitionOffset left, MemoryBufferPartitionOffset right) { var leftGeneration = left.Generation; diff --git a/src/Mocha.Core/Buffer/Memory/MemoryBufferProducer.cs b/src/Mocha.Core/Buffer/Memory/MemoryBufferProducer.cs index a3a5310..bf67eb3 100644 --- a/src/Mocha.Core/Buffer/Memory/MemoryBufferProducer.cs +++ b/src/Mocha.Core/Buffer/Memory/MemoryBufferProducer.cs @@ -3,18 +3,12 @@ namespace Mocha.Core.Buffer.Memory; -internal sealed class MemoryBufferProducer : IBufferProducer +internal sealed class MemoryBufferProducer(string topicName, MemoryBufferPartition[] partitions) + : IBufferProducer { - private readonly MemoryBufferPartition[] _partitions; private uint _partitionIndex; - public MemoryBufferProducer(string topicName, MemoryBufferPartition[] partitions) - { - TopicName = topicName; - _partitions = partitions; - } - - public string TopicName { get; } + public string TopicName { get; } = topicName; public ValueTask ProduceAsync(T item) { @@ -25,7 +19,7 @@ public ValueTask ProduceAsync(T item) private MemoryBufferPartition SelectPartition() { - var index = (Interlocked.Increment(ref _partitionIndex) - 1) % _partitions.Length; - return _partitions[index]; + var index = (Interlocked.Increment(ref _partitionIndex) - 1) % partitions.Length; + return partitions[index]; } } diff --git a/src/Mocha.Core/Buffer/Memory/MemoryBufferSegment.cs b/src/Mocha.Core/Buffer/Memory/MemoryBufferSegment.cs index 7e7308f..bc5117e 100644 --- a/src/Mocha.Core/Buffer/Memory/MemoryBufferSegment.cs +++ b/src/Mocha.Core/Buffer/Memory/MemoryBufferSegment.cs @@ -2,6 +2,7 @@ // The .NET Core Community licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Mocha.Core.Buffer.Memory; @@ -45,6 +46,7 @@ public bool TryEnqueue(T item) var writePosition = Interlocked.Increment(ref _writePosition); if (writePosition >= _slots.Length) { + _writePosition = _slots.Length - 1; return false; } @@ -52,23 +54,25 @@ public bool TryEnqueue(T item) return true; } - public bool TryGet(MemoryBufferPartitionOffset offset, out T item) + public bool TryGet(MemoryBufferPartitionOffset offset, int count, [NotNullWhen(true)] out T[]? items) { if (offset < _startOffset || offset > _endOffset) { - item = default!; + items = null; return false; } - var readPosition = (offset - _startOffset).ToUInt64(); + var readPosition = (offset - _startOffset).ToInt32(); - if (_writePosition < 0 || readPosition > (ulong)_writePosition) + if (_writePosition < 0 || readPosition > _writePosition) { - item = default!; + items = default!; return false; } - item = _slots[readPosition]; + var writePosition = Math.Min(_writePosition, _slots.Length - 1); + var actualCount = Math.Min(count, writePosition - readPosition + 1); + items = _slots[readPosition..(readPosition + actualCount)]; return true; } diff --git a/src/Mocha.Core/Buffer/Memory/PendingDataValueTaskSource.cs b/src/Mocha.Core/Buffer/Memory/PendingDataValueTaskSource.cs index 6766977..56f59fc 100644 --- a/src/Mocha.Core/Buffer/Memory/PendingDataValueTaskSource.cs +++ b/src/Mocha.Core/Buffer/Memory/PendingDataValueTaskSource.cs @@ -8,6 +8,7 @@ namespace Mocha.Core.Buffer.Memory; internal sealed class PendingDataValueTaskSource : IValueTaskSource { private ManualResetValueTaskSourceCore _core = new() { RunContinuationsAsynchronously = true }; + // Default value for ValueTask is a completed task. private ValueTask _valueTask; private volatile bool _isWaiting; diff --git a/src/Mocha.Core/Extensions/DateTimeOffsetExtensions.cs b/src/Mocha.Core/Extensions/DateTimeOffsetExtensions.cs new file mode 100644 index 0000000..bd90652 --- /dev/null +++ b/src/Mocha.Core/Extensions/DateTimeOffsetExtensions.cs @@ -0,0 +1,7 @@ +namespace Mocha.Core.Extensions; + +public static class DateTimeOffsetExtensions +{ + public static ulong ToUnixTimeNanoseconds(this DateTimeOffset dateTimeOffset) => + (ulong)(dateTimeOffset - DateTimeOffset.UnixEpoch).Ticks * 100; +} diff --git a/src/Mocha.Core/Extensions/JsonExtensions.cs b/src/Mocha.Core/Extensions/JsonExtensions.cs new file mode 100644 index 0000000..b20a707 --- /dev/null +++ b/src/Mocha.Core/Extensions/JsonExtensions.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Mocha.Core.Extensions; + +public static class JsonSerializationExtensions +{ + public static T? FromJson(this string json) + { + return JsonConvert.DeserializeObject(json); + } + + public static string ToJson(this T obj) + { + return JsonConvert.SerializeObject(obj, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + } +} diff --git a/src/Mocha.Core/Mocha.Core.csproj b/src/Mocha.Core/Mocha.Core.csproj index c72f030..6d55706 100644 --- a/src/Mocha.Core/Mocha.Core.csproj +++ b/src/Mocha.Core/Mocha.Core.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable true @@ -9,6 +9,7 @@ + diff --git a/src/Mocha.Core/Models/Trace/MochaAttribute.cs b/src/Mocha.Core/Models/Trace/MochaAttribute.cs new file mode 100644 index 0000000..3d1c3cf --- /dev/null +++ b/src/Mocha.Core/Models/Trace/MochaAttribute.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Models.Trace; + +public class MochaAttribute +{ + public required string Key { get; init; } + + public MochaAttributeValueType ValueType { get; init; } + + public required string Value { get; init; } +} diff --git a/src/Mocha.Core/Models/Trace/MochaAttributeValueType.cs b/src/Mocha.Core/Models/Trace/MochaAttributeValueType.cs new file mode 100644 index 0000000..bc047cd --- /dev/null +++ b/src/Mocha.Core/Models/Trace/MochaAttributeValueType.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Models.Trace; + +public enum MochaAttributeValueType +{ + None = 0, + StringValue = 1, + BoolValue = 2, + IntValue = 3, + DoubleValue = 4, + ArrayValue = 5, + KvlistValue = 6, + BytesValue = 7, +} diff --git a/src/Mocha.Core/Models/Trace/MochaResource.cs b/src/Mocha.Core/Models/Trace/MochaResource.cs new file mode 100644 index 0000000..fc73e80 --- /dev/null +++ b/src/Mocha.Core/Models/Trace/MochaResource.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Models.Trace; + +public class MochaResource +{ + public required string ServiceName { get; init; } + + public required string ServiceInstanceId { get; init; } + + public required IEnumerable Attributes { get; init; } +} diff --git a/src/Mocha.Core/Models/Trace/MochaSpan.cs b/src/Mocha.Core/Models/Trace/MochaSpan.cs new file mode 100644 index 0000000..20542a1 --- /dev/null +++ b/src/Mocha.Core/Models/Trace/MochaSpan.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Models.Trace; + +public class MochaSpan +{ + public required string TraceId { get; init; } + + public required string SpanId { get; init; } + + public required string SpanName { get; init; } = string.Empty; + + public required string ParentSpanId { get; init; } = string.Empty; + + public ulong StartTimeUnixNano { get; init; } + + public ulong EndTimeUnixNano { get; init; } + + public ulong DurationNanoseconds { get; init; } + + public MochaSpanStatusCode? StatusCode { get; init; } + + public string? StatusMessage { get; init; } + + public MochaSpanKind SpanKind { get; init; } + + public required MochaResource Resource { get; init; } + + public uint TraceFlags { get; init; } + + public string? TraceState { get; init; } + + public required IEnumerable Links { get; init; } + + public required IEnumerable Attributes { get; init; } + + public required IEnumerable Events { get; init; } +} diff --git a/src/Mocha.Core/Models/Trace/MochaSpanEvent.cs b/src/Mocha.Core/Models/Trace/MochaSpanEvent.cs new file mode 100644 index 0000000..a0c2c1e --- /dev/null +++ b/src/Mocha.Core/Models/Trace/MochaSpanEvent.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Models.Trace; + +public class MochaSpanEvent +{ + public required string Name { get; init; } + + public required IEnumerable Attributes { get; init; } + + public ulong TimestampUnixNano { get; init; } +} diff --git a/src/Mocha.Core/Enums/SpanKind.cs b/src/Mocha.Core/Models/Trace/MochaSpanKind.cs similarity index 70% rename from src/Mocha.Core/Enums/SpanKind.cs rename to src/Mocha.Core/Models/Trace/MochaSpanKind.cs index 23158c6..5c2b552 100644 --- a/src/Mocha.Core/Enums/SpanKind.cs +++ b/src/Mocha.Core/Models/Trace/MochaSpanKind.cs @@ -1,19 +1,14 @@ // Licensed to the .NET Core Community under one or more agreements. // The .NET Core Community licenses this file to you under the MIT license. -namespace Mocha.Core.Enums; +namespace Mocha.Core.Models.Trace; -public enum SpanKind +public enum MochaSpanKind { Unspecified = 0, - - Client = 1, - + Internal = 1, Server = 2, - - Internal = 3, - + Client = 3, Producer = 4, - Consumer = 5 } diff --git a/src/Mocha.Core/Models/Trace/MochaSpanLink.cs b/src/Mocha.Core/Models/Trace/MochaSpanLink.cs new file mode 100644 index 0000000..e97db6d --- /dev/null +++ b/src/Mocha.Core/Models/Trace/MochaSpanLink.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Models.Trace; + +public class MochaSpanLink +{ + public long Id { get; init; } + + public required string LinkedTraceId { get; init; } + + public required string LinkedSpanId { get; init; } + + public required IEnumerable Attributes { get; init; } + + public required string LinkedTraceState { get; init; } + + public uint LinkedTraceFlags { get; init; } +} diff --git a/src/Mocha.Core/Models/Trace/MochaSpanStatusCode.cs b/src/Mocha.Core/Models/Trace/MochaSpanStatusCode.cs new file mode 100644 index 0000000..d3a2c25 --- /dev/null +++ b/src/Mocha.Core/Models/Trace/MochaSpanStatusCode.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Models.Trace; + +public enum MochaSpanStatusCode +{ + Unset = 0, + Ok = 1, + Error = 2, +} diff --git a/src/Mocha.Core/Models/Trace/OTelToMochaSpanConversionExtensions.cs b/src/Mocha.Core/Models/Trace/OTelToMochaSpanConversionExtensions.cs new file mode 100644 index 0000000..71ff849 --- /dev/null +++ b/src/Mocha.Core/Models/Trace/OTelToMochaSpanConversionExtensions.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using Google.Protobuf; +using OpenTelemetry.Proto.Common.V1; +using OpenTelemetry.Proto.Resource.V1; +using OpenTelemetry.Proto.Trace.V1; + +namespace Mocha.Core.Models.Trace; + +public static class OTelToMochaSpanConversionExtensions +{ + public static MochaSpan ToMochaSpan(this Span span, Resource resource) + { + var traceId = ConvertByteStringToTraceId(span.TraceId); + var spanId = ConvertByteStringToSpanId(span.SpanId); + var parentSpanId = ConvertByteStringToSpanId(span.ParentSpanId); + // TODO: no span link found from request + var spanLinks = span.Links.Select(link => link.ToMochaSpanLink()).ToList(); + var spanEvents = span.Events.Select(@event => @event.ToMochaSpanEvent()).ToList(); + var attributes = + span.Attributes.Select(attribute => attribute.ToMochaAttribute()).ToList(); + var serviceName = resource.Attributes + .FirstOrDefault(attribute => attribute.Key == "service.name")? + .Value?.StringValue ?? string.Empty; + var serviceInstanceId = resource.Attributes + .FirstOrDefault(attribute => attribute.Key == "service.instance.id")? + .Value?.StringValue ?? string.Empty; + + var mochaSpan = new MochaSpan + { + SpanId = spanId, + TraceFlags = span.Flags, + TraceId = traceId, + SpanName = span.Name, + ParentSpanId = parentSpanId, + StartTimeUnixNano = span.StartTimeUnixNano, + EndTimeUnixNano = span.EndTimeUnixNano, + DurationNanoseconds = span.EndTimeUnixNano - span.StartTimeUnixNano, + StatusCode = (MochaSpanStatusCode?)span.Status?.Code, + StatusMessage = span.Status?.Message, + SpanKind = (MochaSpanKind)span.Kind, + TraceState = span.TraceState, + Attributes = attributes, + Events = spanEvents, + Links = spanLinks, + Resource = new MochaResource + { + ServiceName = serviceName, + ServiceInstanceId = serviceInstanceId, + Attributes = resource.Attributes.Select(attribute => attribute.ToMochaAttribute()).ToList() + } + }; + + return mochaSpan; + } + + private static MochaSpanEvent ToMochaSpanEvent(this Span.Types.Event @event) + { + return new MochaSpanEvent + { + Name = @event.Name, + Attributes = @event.Attributes.Select(attribute => attribute.ToMochaAttribute()).ToList(), + TimestampUnixNano = @event.TimeUnixNano + }; + } + + private static MochaSpanLink ToMochaSpanLink(this Span.Types.Link link) + { + return new MochaSpanLink + { + LinkedTraceId = ConvertByteStringToTraceId(link.TraceId), + LinkedSpanId = ConvertByteStringToSpanId(link.SpanId), + Attributes = + link.Attributes.Select(attribute => attribute.ToMochaAttribute()).ToList(), + LinkedTraceState = link.TraceState, + LinkedTraceFlags = link.Flags + }; + } + + private static MochaAttribute ToMochaAttribute(this KeyValue attribute) + { + return new MochaAttribute + { + Key = attribute.Key, + ValueType = (MochaAttributeValueType)attribute.Value.ValueCase, + Value = attribute.Value.ValueCase switch + { + AnyValue.ValueOneofCase.StringValue => attribute.Value.StringValue, + AnyValue.ValueOneofCase.BoolValue => attribute.Value.BoolValue.ToString(), + AnyValue.ValueOneofCase.IntValue => attribute.Value.IntValue.ToString(), + AnyValue.ValueOneofCase.DoubleValue => attribute.Value.DoubleValue.ToString("R"), + // TODO: Handle ArrayValue, KvlistValue, and BytesValue + AnyValue.ValueOneofCase.ArrayValue => attribute.Value.ArrayValue.Values.ToString(), + AnyValue.ValueOneofCase.KvlistValue => attribute.Value.KvlistValue.ToString(), + AnyValue.ValueOneofCase.BytesValue => attribute.Value.BytesValue?.ToString() ?? string.Empty, + _ => throw new ArgumentOutOfRangeException(nameof(attribute.Value.ValueCase), + attribute.Value.ValueCase, + "Unknown attribute value case.") + } + }; + } + + private static string ConvertByteStringToSpanId(ByteString byteString) + { + return byteString.Length == 0 ? string.Empty : ConvertBytesToLong(byteString.Span).ToString("x016"); + } + + private static string ConvertByteStringToTraceId(ByteString byteString) + { + if (byteString.Length == 0) + { + return string.Empty; + } + + var high = ConvertBytesToLong(byteString.Span[..8]); + var low = ConvertBytesToLong(byteString.Span[8..16]); + return high == 0 ? low.ToString("x016") : $"{high:x016}{low:x016}"; + } + + private static long ConvertBytesToLong(ReadOnlySpan bytes) => + BitConverter.IsLittleEndian + ? BinaryPrimitives.ReadInt64BigEndian(bytes) + : BinaryPrimitives.ReadInt64LittleEndian(bytes); +} diff --git a/src/Mocha.Core/Storage/ISpanReader.cs b/src/Mocha.Core/Storage/ISpanReader.cs deleted file mode 100644 index 28b5b93..0000000 --- a/src/Mocha.Core/Storage/ISpanReader.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Core Community under one or more agreements. -// The .NET Core Community licenses this file to you under the MIT license. - -using Mocha.Core.Storage.Query; - -namespace Mocha.Core.Storage; - -public interface ISpanReader -{ - Task> FindTraceIdListAsync(TraceReadQuery query); - - Task FindSpanListByTraceIdAsync(string traceId); -} diff --git a/src/Mocha.Core/Storage/ISpanWriter.cs b/src/Mocha.Core/Storage/ISpanWriter.cs index 6cd535b..ff2ab47 100644 --- a/src/Mocha.Core/Storage/ISpanWriter.cs +++ b/src/Mocha.Core/Storage/ISpanWriter.cs @@ -1,11 +1,12 @@ // Licensed to the .NET Core Community under one or more agreements. // The .NET Core Community licenses this file to you under the MIT license. +using Mocha.Core.Models.Trace; using OpenTelemetry.Proto.Trace.V1; namespace Mocha.Core.Storage; public interface ISpanWriter { - Task WriteAsync(IEnumerable spans); + Task WriteAsync(IEnumerable spans); } diff --git a/src/Mocha.Core/Storage/Jaeger/IJaegerSpanReader.cs b/src/Mocha.Core/Storage/Jaeger/IJaegerSpanReader.cs new file mode 100644 index 0000000..24c8c43 --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/IJaegerSpanReader.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Mocha.Core.Storage.Jaeger.Trace; + +namespace Mocha.Core.Storage.Jaeger; + +public interface IJaegerSpanReader +{ + Task GetServicesAsync(); + + Task GetOperationsAsync(string serviceName); + + Task FindTracesAsync(JaegerTraceQueryParameters query); + + Task FindTracesAsync(string[] traceIDs, ulong? startTimeMinUnixNano = null, ulong? startTimeMaxUnixNano = null); +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerProcess.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerProcess.cs new file mode 100644 index 0000000..dca74f1 --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerProcess.cs @@ -0,0 +1,11 @@ +namespace Mocha.Core.Storage.Jaeger.Trace +{ + public class JaegerProcess + { + public required string ProcessID { get; init; } + + public required string ServiceName { get; init; } + + public required JaegerTag[] Tags { get; init; } + } +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpan.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpan.cs new file mode 100644 index 0000000..1a03ca8 --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpan.cs @@ -0,0 +1,25 @@ +namespace Mocha.Core.Storage.Jaeger.Trace +{ + public class JaegerSpan + { + public required string TraceID { get; init; } + + public required string SpanID { get; init; } + + public required string OperationName { get; init; } + + public uint Flags { get; init; } + + public ulong StartTime { get; init; } + + public ulong Duration { get; init; } + + public required string ProcessID { get; init; } + + public required JaegerSpanReference[] References { get; init; } + + public required JaegerTag[] Tags { get; init; } + + public required JaegerSpanLog[] Logs { get; init; } + } +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanKind.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanKind.cs new file mode 100644 index 0000000..25be0d1 --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanKind.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Storage.Jaeger.Trace; + +public static class JaegerSpanKind +{ + public const string Unspecified = "unspecified"; + public const string Internal = "internal"; + public const string Server = "server"; + public const string Client = "client"; + public const string Producer = "producer"; + public const string Consumer = "consumer"; +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanLog.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanLog.cs new file mode 100644 index 0000000..fa9042b --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanLog.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Storage.Jaeger.Trace; + +public class JaegerSpanLog +{ + public ulong Timestamp { get; init; } + + public required JaegerTag[] Fields { get; init; } +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanReference.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanReference.cs new file mode 100644 index 0000000..8386b9b --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanReference.cs @@ -0,0 +1,9 @@ +namespace Mocha.Core.Storage.Jaeger.Trace +{ + public class JaegerSpanReference + { + public required string TraceID { get; init; } + public required string SpanID { get; init; } + public required string RefType { get; init; } + } +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanReferenceType.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanReferenceType.cs new file mode 100644 index 0000000..09deac0 --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerSpanReferenceType.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Storage.Jaeger.Trace; + +public static class JaegerSpanReferenceType +{ + public const string ChildOf = "CHILD_OF"; + public const string FollowsFrom = "FOLLOWS_FROM"; +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTag.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTag.cs new file mode 100644 index 0000000..402e52d --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTag.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Storage.Jaeger.Trace; + +public class JaegerTag +{ + public required string Key { get; set; } + + public required string Type { get; set; } + + public required object Value { get; set; } +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTagType.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTagType.cs new file mode 100644 index 0000000..52e3d4b --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTagType.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Storage.Jaeger.Trace; + +public static class JaegerTagType +{ + public const string String = "string"; + public const string Bool = "bool"; + public const string Int64 = "int64"; + public const string Float64 = "float64"; + public const string Binary = "binary"; +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTrace.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTrace.cs new file mode 100644 index 0000000..5037324 --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTrace.cs @@ -0,0 +1,11 @@ +namespace Mocha.Core.Storage.Jaeger.Trace +{ + public class JaegerTrace + { + public required string TraceID { get; set; } + + public required Dictionary Processes { get; set; } + + public required JaegerSpan[] Spans { get; set; } + } +} diff --git a/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTraceQueryParameters.cs b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTraceQueryParameters.cs new file mode 100644 index 0000000..af66bd1 --- /dev/null +++ b/src/Mocha.Core/Storage/Jaeger/Trace/JaegerTraceQueryParameters.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Core.Storage.Jaeger.Trace; + +public class JaegerTraceQueryParameters +{ + public string? ServiceName { get; init; } + + public string? OperationName { get; init; } + + public Dictionary? Tags { get; init; } + + public ulong? StartTimeMinUnixNano { get; init; } + + public ulong? StartTimeMaxUnixNano { get; init; } + + public ulong? DurationMinNanoseconds { get; init; } + + public ulong? DurationMaxNanoseconds { get; init; } + + public int NumTraces { get; init; } +} diff --git a/src/Mocha.Core/Storage/Query/TraceReadQuery.cs b/src/Mocha.Core/Storage/Query/TraceReadQuery.cs deleted file mode 100644 index 121c60b..0000000 --- a/src/Mocha.Core/Storage/Query/TraceReadQuery.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Core Community under one or more agreements. -// The .NET Core Community licenses this file to you under the MIT license. - -using System.Collections; - -namespace Mocha.Core.Storage.Query; - -public class TraceReadQuery -{ - public string? ServiceName { get; set; } - - public IDictionary? SpanAttributes { get; set; } - - public long? StartTimeStamp { get; set; } - - public long? EndTimeStamp { get; set; } - - public string? SpanName { get; set; } -} diff --git a/src/Mocha.Distributor/Exporters/StorageExporter.cs b/src/Mocha.Distributor/Exporters/StorageExporter.cs new file mode 100644 index 0000000..f995764 --- /dev/null +++ b/src/Mocha.Distributor/Exporters/StorageExporter.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Mocha.Core.Buffer; +using Mocha.Core.Models.Trace; +using Mocha.Core.Storage; + +namespace Mocha.Distributor.Exporters; + +public class StorageExporter( + ISpanWriter spanWriter, + IBufferQueue bufferQueue, + ILogger logger) + : IHostedService +{ + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + public Task StartAsync(CancellationToken cancellationToken) + { + var consumerNumber = Environment.ProcessorCount; + var consumers = bufferQueue.CreateConsumers( + new BufferConsumerOptions + { + TopicName = "otlp-span", + GroupName = "storage-exporter", + AutoCommit = false, + BatchSize = 100 + }, consumerNumber); + + var token = CancellationTokenSource + .CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token).Token; + + foreach (var consumer in consumers) + { + _ = ConsumeAsync(consumer, token); + } + + logger.LogInformation( + "Storage exporter started, consuming from {ConsumerCount} consumers.", consumerNumber); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource.Cancel(); + logger.LogInformation("Storage exporter stopped."); + return Task.CompletedTask; + } + + private async Task ConsumeAsync(IBufferConsumer consumer, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + await foreach (var spans in consumer.ConsumeAsync(cancellationToken)) + { + var tryCount = 0; + while (true) + { + try + { + await spanWriter.WriteAsync(spans); + break; + } + catch (Exception ex) + { + tryCount++; + if (tryCount <= 3) // TODO: Make this configurable. + { + logger.LogWarning(ex, "Failed to write spans to storage, retrying..."); + await Task.Delay(1000, cancellationToken); + } + else + { + logger.LogError(ex, "Failed to write spans to storage."); + break; + } + } + } + + await consumer.CommitAsync(); + } + } + } +} diff --git a/src/Mocha.Distributor/Mocha.Distributor.csproj b/src/Mocha.Distributor/Mocha.Distributor.csproj index 3caec5d..fbdcda1 100644 --- a/src/Mocha.Distributor/Mocha.Distributor.csproj +++ b/src/Mocha.Distributor/Mocha.Distributor.csproj @@ -1,15 +1,16 @@ - net7.0 + net8.0 enable enable true + Linux - - + + diff --git a/src/Mocha.Distributor/Program.cs b/src/Mocha.Distributor/Program.cs index 4fbad35..6444240 100644 --- a/src/Mocha.Distributor/Program.cs +++ b/src/Mocha.Distributor/Program.cs @@ -2,20 +2,16 @@ // The .NET Core Community licenses this file to you under the MIT license. using System.Net; +using Microsoft.EntityFrameworkCore; using Mocha.Core.Buffer; +using Mocha.Core.Models.Trace; +using Mocha.Distributor.Exporters; using Mocha.Distributor.Services; -using OpenTelemetry.Proto.Trace.V1; +using Mocha.Storage; +using Mocha.Storage.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); -// builder.Configuration.AddEnvironmentVariables(prefix: "Mocha"); - -builder.WebHost.ConfigureKestrel(options => -{ - var port = builder.Configuration.GetValue("OTel:Grpc:Server:Port"); - options.Listen(IPAddress.Any, port); -}); - // Additional configuration is required to successfully run gRPC on macOS. // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 @@ -27,10 +23,22 @@ { options.UseMemory(bufferOptions => { - bufferOptions.AddTopic("otlp_spans", Environment.ProcessorCount); + bufferOptions.AddTopic("otlp-span", Environment.ProcessorCount); + }); +}); + +builder.Services.AddStorage(options => +{ + options.UseEntityFrameworkCore(efOptions => + { + var connectionString = builder.Configuration.GetConnectionString("EF"); + var serverVersion = ServerVersion.AutoDetect(connectionString); + efOptions.UseMySql(connectionString, serverVersion); }); }); +builder.Services.AddHostedService(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/src/Mocha.Distributor/Services/OTelTraceExportService.cs b/src/Mocha.Distributor/Services/OTelTraceExportService.cs index 0829862..aaa6f7f 100644 --- a/src/Mocha.Distributor/Services/OTelTraceExportService.cs +++ b/src/Mocha.Distributor/Services/OTelTraceExportService.cs @@ -2,14 +2,48 @@ // The .NET Core Community licenses this file to you under the MIT license. using Grpc.Core; +using Mocha.Core.Buffer; +using Mocha.Core.Models.Trace; using OpenTelemetry.Proto.Collector.Trace.V1; namespace Mocha.Distributor.Services; -public class OTelTraceExportService : TraceService.TraceServiceBase +public class OTelTraceExportService(IBufferQueue bufferQueue) : TraceService.TraceServiceBase { - public override Task Export(ExportTraceServiceRequest request, ServerCallContext context) + private readonly IBufferProducer _bufferProducer = + bufferQueue.CreateProducer("otlp-span"); + + public override async Task Export( + ExportTraceServiceRequest request, + ServerCallContext context) { - return Task.FromResult(new ExportTraceServiceResponse()); + var spans = request.ResourceSpans + .SelectMany(resourceSpans => resourceSpans.ScopeSpans + .SelectMany(scopeSpans => scopeSpans.Spans + .Select(span => span.ToMochaSpan(resourceSpans.Resource)))).ToArray(); + + var totalSpanCount = spans.Length; + var acceptedSpanCount = 0; + + try + { + for (; acceptedSpanCount < totalSpanCount; acceptedSpanCount++) + { + await _bufferProducer.ProduceAsync(spans[acceptedSpanCount]); + } + } + catch (Exception ex) + { + return new ExportTraceServiceResponse + { + PartialSuccess = new ExportTracePartialSuccess + { + RejectedSpans = totalSpanCount - acceptedSpanCount, + ErrorMessage = ex.Message + } + }; + } + + return new ExportTraceServiceResponse(); } } diff --git a/src/Mocha.Distributor/appsettings.Development.json b/src/Mocha.Distributor/appsettings.Development.json index 864be1c..4589a72 100644 --- a/src/Mocha.Distributor/appsettings.Development.json +++ b/src/Mocha.Distributor/appsettings.Development.json @@ -5,11 +5,7 @@ "Microsoft.AspNetCore": "Warning" } }, - "OTel": { - "Grpc": { - "Server": { - "Port": 4319 - } - } + "ConnectionStrings": { + "EF": "server=xxx;userid=xxx;password=xxx;database=mocha;" } } diff --git a/src/Mocha.Distributor/appsettings.json b/src/Mocha.Distributor/appsettings.json index 5239662..b3369ff 100644 --- a/src/Mocha.Distributor/appsettings.json +++ b/src/Mocha.Distributor/appsettings.json @@ -9,13 +9,12 @@ "Kestrel": { "EndpointDefaults": { "Protocols": "Http2" - } - }, - "OTel": { - "Grpc": { - "Server": { - "Port": 4317 + }, + "Endpoints": { + "OTelGrpcEndpoint": { + "Url": "http://*:4317" } } } } + diff --git a/src/Mocha.Protocol.Generated/Mocha.Protocol.Generated.csproj b/src/Mocha.Protocol.Generated/Mocha.Protocol.Generated.csproj index 1e914ab..fc516fd 100644 --- a/src/Mocha.Protocol.Generated/Mocha.Protocol.Generated.csproj +++ b/src/Mocha.Protocol.Generated/Mocha.Protocol.Generated.csproj @@ -1,16 +1,16 @@ - net7.0 + net8.0 enable enable true - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Mocha.Query.Jaeger/Controllers/TraceController.cs b/src/Mocha.Query.Jaeger/Controllers/TraceController.cs new file mode 100644 index 0000000..6ad6ada --- /dev/null +++ b/src/Mocha.Query.Jaeger/Controllers/TraceController.cs @@ -0,0 +1,124 @@ +using System.Net; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc; +using Mocha.Core.Extensions; +using Mocha.Core.Storage.Jaeger; +using Mocha.Core.Storage.Jaeger.Trace; +using Mocha.Query.Jaeger.DTOs; + +namespace Mocha.Query.Jaeger.Controllers +{ + [Route("/api")] + public class TraceController(IJaegerSpanReader spanReader) : Controller + { + [HttpGet("services")] + public async Task> GetSeries() + { + return new(await spanReader.GetServicesAsync()); + } + + [HttpGet("services/{serviceName}/operations")] + public async Task> GetOperations(string serviceName) + { + return new(await spanReader.GetOperationsAsync(serviceName)); + } + + [HttpGet("traces")] + public async Task> FindTraces([FromQuery] FindTracesRequest request) + { + static ulong? ParseAsNanoseconds(string? input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return null; + } + + var m = Regex.Match(input, + @"^((?\d+)d)?((?\d+)h)?((?\d+)m)?((?\d+)s)?((?\d+)ms)?((?\d+)μs)?$", + RegexOptions.ExplicitCapture + | RegexOptions.Compiled + | RegexOptions.CultureInvariant + | RegexOptions.RightToLeft); + + if (!m.Success) + { + return null; + } + + var days = m.Groups["days"].Success ? long.Parse(m.Groups["days"].Value) : 0; + var hours = m.Groups["hours"].Success ? long.Parse(m.Groups["hours"].Value) : 0; + var minutes = m.Groups["minutes"].Success ? long.Parse(m.Groups["minutes"].Value) : 0; + var seconds = m.Groups["seconds"].Success ? long.Parse(m.Groups["seconds"].Value) : 0; + var milliseconds = m.Groups["milliseconds"].Success ? long.Parse(m.Groups["milliseconds"].Value) : 0; + var microseconds = m.Groups["microseconds"].Success ? long.Parse(m.Groups["microseconds"].Value) : 0; + + return + (ulong)(((days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds) * 1000 + milliseconds) + * 1000 + microseconds) * 1000; + } + + var startTimeMin = request.Start * 1000; + + + var startTimeMax = request.End * 1000; + + var lookBack = ParseAsNanoseconds(request.LookBack); + + if (lookBack.HasValue) + { + var now = DateTimeOffset.Now.ToUnixTimeNanoseconds(); + startTimeMin = now - lookBack.Value; + startTimeMax = now; + } + + JaegerTrace[] traces; + + if (request.TraceID?.Any() ?? false) + { + traces = await spanReader.FindTracesAsync(request.TraceID, startTimeMin, startTimeMax); + } + else + { + traces = await spanReader.FindTracesAsync(new JaegerTraceQueryParameters + { + ServiceName = request.Service, + OperationName = request.Operation, + Tags = (request.Tags ?? "{}").FromJson>()!, + StartTimeMinUnixNano = startTimeMin, + StartTimeMaxUnixNano = startTimeMax, + DurationMinNanoseconds = + string.IsNullOrWhiteSpace(request.MinDuration) + ? null + : ParseAsNanoseconds(request.MinDuration)!, + DurationMaxNanoseconds = + string.IsNullOrWhiteSpace(request.MaxDuration) + ? null + : ParseAsNanoseconds(request.MaxDuration)!, + NumTraces = request.Limit + }); + } + + JaegerResponseError? error = null; + if (traces.Length == 0) + { + error = new JaegerResponseError { Code = (int)HttpStatusCode.NotFound, Message = "trace not found" }; + } + + return new JaegerResponse(traces) { Error = error }; + } + + [HttpGet("traces/{traceID}")] + public async Task> GetTrace(string traceID) + { + var traces = await spanReader.FindTracesAsync([traceID]); + + JaegerResponseError? error = null; + if (traces.Length == 0) + { + error = new JaegerResponseError { Code = (int)HttpStatusCode.NotFound, Message = "trace not found" }; + } + + return new JaegerResponse(traces) { Error = error }; + } + } +} diff --git a/src/Mocha.Query.Jaeger/DTOs/FindTracesRequest.cs b/src/Mocha.Query.Jaeger/DTOs/FindTracesRequest.cs new file mode 100644 index 0000000..cbef4c1 --- /dev/null +++ b/src/Mocha.Query.Jaeger/DTOs/FindTracesRequest.cs @@ -0,0 +1,25 @@ +namespace Mocha.Query.Jaeger.DTOs +{ + public class FindTracesRequest + { + public string[]? TraceID { get; set; } + + public string? Service { get; set; } + + public string? Operation { get; set; } + + public string? Tags { get; set; } + + public string? LookBack { get; set; } + + public ulong? Start { get; set; } + + public ulong? End { get; set; } + + public string? MinDuration { get; set; } + + public string? MaxDuration { get; set; } + + public int Limit { get; set; } + } +} diff --git a/src/Mocha.Query.Jaeger/DTOs/JaegerResponse.cs b/src/Mocha.Query.Jaeger/DTOs/JaegerResponse.cs new file mode 100644 index 0000000..c7d16bb --- /dev/null +++ b/src/Mocha.Query.Jaeger/DTOs/JaegerResponse.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Query.Jaeger.DTOs; + +public class JaegerResponse(T data) +{ + public JaegerResponseError? Error { get; init; } + public T Data { get; init; } = data; +} diff --git a/src/Mocha.Query.Jaeger/DTOs/JaegerResponseError.cs b/src/Mocha.Query.Jaeger/DTOs/JaegerResponseError.cs new file mode 100644 index 0000000..fde67a2 --- /dev/null +++ b/src/Mocha.Query.Jaeger/DTOs/JaegerResponseError.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Query.Jaeger.DTOs; + +public class JaegerResponseError +{ + public int Code { get; init; } + public required string Message { get; init; } +} diff --git a/src/Mocha.Query.Jaeger/Mocha.Query.Jaeger.csproj b/src/Mocha.Query.Jaeger/Mocha.Query.Jaeger.csproj new file mode 100644 index 0000000..778dcf6 --- /dev/null +++ b/src/Mocha.Query.Jaeger/Mocha.Query.Jaeger.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + true + Linux + + + + + + + + + + + + + diff --git a/src/Mocha.Query.Jaeger/Program.cs b/src/Mocha.Query.Jaeger/Program.cs new file mode 100644 index 0000000..8e098e8 --- /dev/null +++ b/src/Mocha.Query.Jaeger/Program.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using Mocha.Storage; +using Mocha.Storage.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddControllers(); + +builder.Services.AddStorage(options => +{ + options.UseEntityFrameworkCore(efOptions => + { + var connectionString = builder.Configuration.GetConnectionString("EF"); + var serverVersion = ServerVersion.AutoDetect(connectionString); + efOptions.UseMySql(connectionString, serverVersion); + }); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapControllers(); + +app.Run(); diff --git a/src/Mocha.Query.Jaeger/Properties/launchSettings.json b/src/Mocha.Query.Jaeger/Properties/launchSettings.json new file mode 100644 index 0000000..5068857 --- /dev/null +++ b/src/Mocha.Query.Jaeger/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:63245", + "sslPort": 44361 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5775", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7031;http://localhost:5775", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Mocha.Query.Jaeger/appsettings.Development.json b/src/Mocha.Query.Jaeger/appsettings.Development.json new file mode 100644 index 0000000..4589a72 --- /dev/null +++ b/src/Mocha.Query.Jaeger/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "EF": "server=xxx;userid=xxx;password=xxx;database=mocha;" + } +} diff --git a/src/Mocha.Query.Jaeger/appsettings.json b/src/Mocha.Query.Jaeger/appsettings.json new file mode 100644 index 0000000..991b5cb --- /dev/null +++ b/src/Mocha.Query.Jaeger/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "JaegerQueryEndpoint": { + "Url": "http://*:5775" + } + } + } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Configurations/ResourceAttributeConfiguration.cs b/src/Mocha.Storage/EntityFrameworkCore/Configurations/ResourceAttributeConfiguration.cs new file mode 100644 index 0000000..51f6ea0 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Configurations/ResourceAttributeConfiguration.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Mocha.Core.Models.Trace; +using Mocha.Storage.EntityFrameworkCore.Trace; + +namespace Mocha.Storage.EntityFrameworkCore.Configurations; + +public class ResourceAttributeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("resource_attribute"); + builder.HasKey(e => e.Id); + builder.Property(e => e.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(e => e.TraceId).HasColumnName("trace_id").IsRequired(); + builder.Property(e => e.SpanId).HasColumnName("span_id").IsRequired(); + builder.Property(e => e.Key).HasColumnName("key").IsRequired(); + builder.Property(e => e.ValueType).HasColumnName("value_type").IsRequired(); + builder.Property(e => e.Value).HasColumnName("value").IsRequired(); + } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanAttributeConfiguration.cs b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanAttributeConfiguration.cs index 7fafe44..5d83323 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanAttributeConfiguration.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanAttributeConfiguration.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Mocha.Core.Models.Trace; using Mocha.Storage.EntityFrameworkCore.Trace; namespace Mocha.Storage.EntityFrameworkCore.Configurations; @@ -11,12 +12,13 @@ public class SpanAttributeConfiguration : IEntityTypeConfiguration builder) { + builder.ToTable("span_attribute"); builder.HasKey(e => e.Id); - builder.Property(e => e.Id).ValueGeneratedOnAdd().HasColumnType("bigint AUTO_INCREMENT"); - builder.HasIndex(x => x.SpanId, "idx_span_id"); - builder.HasIndex(x => x.TraceId, "idx_trace_id"); - builder.HasIndex(x => x.AttributeKey, "idx_attribute_key"); - builder.HasIndex(x => x.AttributeValue, "idx_attribute_value"); - builder.ToTable("span_attributes"); + builder.Property(e => e.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(e => e.TraceId).HasColumnName("trace_id").IsRequired(); + builder.Property(e => e.SpanId).HasColumnName("span_id").IsRequired(); + builder.Property(e => e.Key).HasColumnName("key").IsRequired(); + builder.Property(e => e.ValueType).HasColumnName("value_type").IsRequired(); + builder.Property(e => e.Value).HasColumnName("value").IsRequired(); } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanConfiguration.cs b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanConfiguration.cs index 9f653c9..406e8a7 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanConfiguration.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanConfiguration.cs @@ -11,19 +11,22 @@ public class SpanConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + builder.ToTable("span"); builder.HasKey(e => e.Id); - builder.Property(e => e.Id).ValueGeneratedOnAdd().HasColumnType("bigint AUTO_INCREMENT"); - builder.HasIndex(x => x.SpanId, "idx_span_id"); - builder.HasIndex(x => x.TraceId, "idx_trace_id"); - builder.HasMany(config => config.SpanAttributes) - .WithOne() - .OnDelete(DeleteBehavior.Cascade); - builder.HasMany(config => config.SpanEvents) - .WithOne() - .OnDelete(DeleteBehavior.Cascade); - builder.HasMany(config => config.SpanLinks) - .WithOne() - .OnDelete(DeleteBehavior.Cascade); - builder.ToTable("spans"); + builder.Property(e => e.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(e => e.TraceId).HasColumnName("trace_id").IsRequired(); + builder.Property(e => e.SpanId).HasColumnName("span_id").IsRequired(); + builder.Property(e => e.SpanName).HasColumnName("span_name").IsRequired(); + builder.Property(e => e.ParentSpanId).HasColumnName("parent_span_id"); + builder.Property(e => e.StartTimeUnixNano).HasColumnName("start_time_unix_nano").IsRequired(); + builder.Property(e => e.EndTimeUnixNano).HasColumnName("end_time_unix_nano").IsRequired(); + builder.Property(e => e.DurationNanoseconds).HasColumnName("duration_nanoseconds").IsRequired(); + builder.Property(e => e.StatusCode).HasColumnName("status_code"); + builder.Property(e => e.StatusMessage).HasColumnName("status_message"); + builder.Property(e => e.SpanKind).HasColumnName("span_kind").IsRequired(); + builder.Property(e => e.ServiceName).HasColumnName("service_name").IsRequired(); + builder.Property(e => e.ServiceInstanceId).HasColumnName("service_instance_id").IsRequired(); + builder.Property(e => e.TraceFlags).HasColumnName("trace_flags").IsRequired(); + builder.Property(e => e.TraceState).HasColumnName("trace_state").IsRequired(); } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanEventAttributeConfiguration.cs b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanEventAttributeConfiguration.cs new file mode 100644 index 0000000..eaf8e45 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanEventAttributeConfiguration.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Mocha.Core.Models.Trace; +using Mocha.Storage.EntityFrameworkCore.Trace; + +namespace Mocha.Storage.EntityFrameworkCore.Configurations; + +public class SpanEventAttributeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("span_event_attribute"); + builder.HasKey(e => e.Id); + builder.Property(e => e.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(e => e.TraceId).HasColumnName("trace_id").IsRequired(); + builder.Property(e => e.SpanId).HasColumnName("span_id").IsRequired(); + builder.Property(e => e.SpanEventIndex).HasColumnName("span_event_index").IsRequired(); + builder.Property(e => e.Key).HasColumnName("key").IsRequired(); + builder.Property(e => e.ValueType).HasColumnName("value_type").IsRequired(); + builder.Property(e => e.Value).HasColumnName("value").IsRequired(); + } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanEventConfiguration.cs b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanEventConfiguration.cs index e140d19..1301998 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanEventConfiguration.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanEventConfiguration.cs @@ -11,10 +11,13 @@ public class SpanEventConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + builder.ToTable("span_event"); builder.HasKey(e => e.Id); - builder.Property(e => e.Id).ValueGeneratedOnAdd().HasColumnType("bigint AUTO_INCREMENT"); - builder.HasIndex(x => x.TraceId, "idx_trace_id"); - builder.HasIndex(x => x.EventName, "idx_event_name"); - builder.ToTable("span_events"); + builder.Property(e => e.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(e => e.TraceId).HasColumnName("trace_id").IsRequired(); + builder.Property(e => e.SpanId).HasColumnName("span_id").IsRequired(); + builder.Property(e => e.Index).HasColumnName("index").IsRequired(); + builder.Property(e => e.Name).HasColumnName("name").IsRequired(); + builder.Property(e => e.TimestampUnixNano).HasColumnName("timestamp_unix_nano").IsRequired(); } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanLinkAttributeConfiguration.cs b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanLinkAttributeConfiguration.cs new file mode 100644 index 0000000..a628c1c --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanLinkAttributeConfiguration.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Mocha.Core.Models.Trace; +using Mocha.Storage.EntityFrameworkCore.Trace; + +namespace Mocha.Storage.EntityFrameworkCore.Configurations; + +public class SpanLinkAttributeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("span_link_attribute"); + builder.HasKey(e => e.Id); + builder.Property(e => e.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(e => e.TraceId).HasColumnName("trace_id").IsRequired(); + builder.Property(e => e.SpanId).HasColumnName("span_id").IsRequired(); + builder.Property(e => e.SpanLinkIndex).HasColumnName("span_link_index").IsRequired(); + builder.Property(e => e.Key).HasColumnName("key").IsRequired(); + builder.Property(e => e.ValueType).HasColumnName("value_type").IsRequired(); + builder.Property(e => e.Value).HasColumnName("value").IsRequired(); + } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanLinkConfiguration.cs b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanLinkConfiguration.cs index 3f51b8f..1978ab3 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanLinkConfiguration.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Configurations/SpanLinkConfiguration.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Mocha.Core.Models.Trace; using Mocha.Storage.EntityFrameworkCore.Trace; namespace Mocha.Storage.EntityFrameworkCore.Configurations; @@ -11,11 +12,15 @@ public class SpanLinkConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + builder.ToTable("span_link"); builder.HasKey(e => e.Id); - builder.Property(e => e.Id).ValueGeneratedOnAdd().HasColumnType("bigint AUTO_INCREMENT"); - builder.HasIndex(x => x.SpanId, "idx_span_id"); - builder.HasIndex(x => x.TraceId, "idx_trace_id"); - builder.HasIndex(x => x.LinkedSpanId, "idx_linked_span_id"); - builder.ToTable("span_links"); + builder.Property(e => e.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(e => e.TraceId).HasColumnName("trace_id").IsRequired(); + builder.Property(e => e.SpanId).HasColumnName("span_id").IsRequired(); + builder.Property(e => e.Index).HasColumnName("index").IsRequired(); + builder.Property(e => e.LinkedTraceId).HasColumnName("linked_trace_id").IsRequired(); + builder.Property(e => e.LinkedSpanId).HasColumnName("linked_span_id").IsRequired(); + builder.Property(e => e.LinkedTraceState).HasColumnName("linked_trace_state").IsRequired(); + builder.Property(e => e.LinkedTraceFlags).HasColumnName("linked_trace_flags").IsRequired(); } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/EFConversionExtensions.cs b/src/Mocha.Storage/EntityFrameworkCore/EFConversionExtensions.cs deleted file mode 100644 index 7c07f59..0000000 --- a/src/Mocha.Storage/EntityFrameworkCore/EFConversionExtensions.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Licensed to the .NET Core Community under one or more agreements. -// The .NET Core Community licenses this file to you under the MIT license. - -using System.Text; -using Mocha.Storage.EntityFrameworkCore.Trace; - -namespace Mocha.Storage.EntityFrameworkCore; - -public static class EFConversionExtensions -{ - - public static EFSpan ToEFSpan(this OpenTelemetry.Proto.Trace.V1.Span span) - { - var traceId = Encoding.UTF8.GetString(span.TraceId.ToByteArray()); - var spanId = Encoding.UTF8.GetString(span.SpanId.ToByteArray()); - var parentSpanId = Encoding.UTF8.GetString(span.ParentSpanId.ToByteArray()); - var entityFrameworkSpan = new EFSpan() - { - SpanId = spanId, - TraceFlags = span.Flags, - TraceId = traceId, - SpanName = span.Name, - ParentSpanId = parentSpanId, - StartTime = (long)span.StartTimeUnixNano, - EndTime = (long)span.EndTimeUnixNano, - Duration = (double)span.EndTimeUnixNano - span.StartTimeUnixNano, - StatusCode = (int)span.Status.Code, - StatusMessage = span.Status.Message, - TraceState = span.TraceState, - SpanKind = span.Kind.ToMochaSpanKind(), - }; - var spanLinks = span.Links.Select(link => link.ToEFSpanLink(traceId)); - var spanEvents = span.Events.Select(@event => @event.ToEFSpanEvent(traceId)); - var spanAttributes = span.Attributes.Select(attribute => attribute.ToEFSpanAttribute(traceId, spanId)); - entityFrameworkSpan.SpanAttributes = spanAttributes.ToList(); - entityFrameworkSpan.SpanEvents = spanEvents.ToList(); - entityFrameworkSpan.SpanLinks = spanLinks.ToList(); - return entityFrameworkSpan; - } - - private static EFSpanAttribute ToEFSpanAttribute(this OpenTelemetry.Proto.Common.V1.KeyValue keyValue, string traceId, - string spanId) - { - return new EFSpanAttribute - { - AttributeKey = keyValue.Key, - AttributeValue = keyValue.Value.StringValue, - SpanId = spanId, - TraceId = traceId, - }; - } - - private static EFSpanEvent ToEFSpanEvent(this OpenTelemetry.Proto.Trace.V1.Span.Types.Event @event, string traceId) - { - return new EFSpanEvent - { - TraceId = traceId, - EventName = @event.Name, - TimeBucket = (long)@event.TimeUnixNano - }; - } - - private static EFSpanLink ToEFSpanLink(this OpenTelemetry.Proto.Trace.V1.Span.Types.Link link, string traceId) - { - return new EFSpanLink - { - TraceId = link.TraceId.ToString() ?? string.Empty, - SpanId = link.SpanId.ToString() ?? string.Empty, - LinkedSpanId = traceId, - TraceState = link.TraceState, - Flags = link.Flags - }; - } -} diff --git a/src/Mocha.Storage/EntityFrameworkCore/EFOptionsBuilderExtensions.cs b/src/Mocha.Storage/EntityFrameworkCore/EFOptionsBuilderExtensions.cs index 3c597b8..e06eea5 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/EFOptionsBuilderExtensions.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/EFOptionsBuilderExtensions.cs @@ -4,6 +4,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Mocha.Core.Storage; +using Mocha.Core.Storage.Jaeger; +using Mocha.Storage.EntityFrameworkCore.Jaeger; namespace Mocha.Storage.EntityFrameworkCore; @@ -13,8 +15,9 @@ public static StorageOptionsBuilder UseEntityFrameworkCore( this StorageOptionsBuilder builder, Action optionsAction) { - builder.Services.AddScoped(); - builder.Services.AddDbContextPool(optionsAction); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddPooledDbContextFactory(optionsAction); return builder; } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/EFSpanWriter.cs b/src/Mocha.Storage/EntityFrameworkCore/EFSpanWriter.cs index f7e184c..821820c 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/EFSpanWriter.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/EFSpanWriter.cs @@ -1,23 +1,61 @@ // Licensed to the .NET Core Community under one or more agreements. // The .NET Core Community licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore; +using Mocha.Core.Models.Trace; using Mocha.Core.Storage; +using Mocha.Storage.EntityFrameworkCore.Trace; namespace Mocha.Storage.EntityFrameworkCore; -public class EFSpanWriter : ISpanWriter +internal class EFSpanWriter(IDbContextFactory factory) : ISpanWriter { - private readonly MochaContext _mochaContext; - - public EFSpanWriter(MochaContext mochaContext) + public async Task WriteAsync(IEnumerable spans) { - _mochaContext = mochaContext; - } + var efSpans = new List(); + var efSpanAttributes = new List(); + var efResourceAttributes = new List(); + var efSpanEvents = new List(); + var efSpanEventAttributes = new List(); + var efSpanLinks = new List(); + var efSpanLinkAttributes = new List(); - public async Task WriteAsync(IEnumerable spans) - { - var entityFrameworkSpans = spans.Select(span => span.ToEFSpan()); - _mochaContext.Spans.AddRange(entityFrameworkSpans); - await _mochaContext.SaveChangesAsync(); + foreach (var span in spans) + { + var efSpan = span.ToEFSpan(); + efSpans.Add(efSpan); + efSpanAttributes.AddRange(span.ToEFSpanAttributes()); + efResourceAttributes.AddRange(span.ToEFResourceAttributes()); + + var spanEvents = span.Events.ToArray(); + for (var i = 0; i < spanEvents.Length; i++) + { + var spanEvent = spanEvents[i]; + var efSpanEvent = spanEvent.ToEFSpanEvent(span, i); + efSpanEvents.Add(efSpanEvent); + efSpanEventAttributes.AddRange(spanEvent.ToEFSpanEventAttributes(efSpanEvent)); + } + + var spanLinks = span.Links.ToArray(); + for (var i = 0; i < spanLinks.Length; i++) + { + var spanLink = spanLinks[i]; + var efSpanLink = spanLink.ToEFSpanLink(span, i); + efSpanLinks.Add(efSpanLink); + efSpanLinkAttributes.AddRange(spanLink.ToEFSpanLinkAttributes(efSpanLink)); + } + } + + await using var context = await factory.CreateDbContextAsync(); + + await context.Spans.AddRangeAsync(efSpans); + await context.SpanAttributes.AddRangeAsync(efSpanAttributes); + await context.ResourceAttributes.AddRangeAsync(efResourceAttributes); + await context.SpanEvents.AddRangeAsync(efSpanEvents); + await context.SpanEventAttributes.AddRangeAsync(efSpanEventAttributes); + await context.SpanLinks.AddRangeAsync(efSpanLinks); + await context.SpanLinkAttributes.AddRangeAsync(efSpanLinkAttributes); + + await context.SaveChangesAsync(); } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Jaeger/EFJaegerSpanReader.cs b/src/Mocha.Storage/EntityFrameworkCore/Jaeger/EFJaegerSpanReader.cs new file mode 100644 index 0000000..1b87190 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Jaeger/EFJaegerSpanReader.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Mocha.Core.Storage.Jaeger; +using Mocha.Core.Storage.Jaeger.Trace; +using Mocha.Storage.EntityFrameworkCore.Trace; + +namespace Mocha.Storage.EntityFrameworkCore.Jaeger; + +internal class EFJaegerSpanReader(IDbContextFactory contextFactory) : IJaegerSpanReader +{ + public async Task GetServicesAsync() + { + await using var context = await contextFactory.CreateDbContextAsync(); + var services = await context.Spans.Select(s => s.ServiceName).Distinct().ToArrayAsync(); + return services; + } + + public async Task GetOperationsAsync(string serviceName) + { + await using var context = await contextFactory.CreateDbContextAsync(); + var operations = await context.Spans + .Where(s => s.ServiceName == serviceName) + .Select(s => s.SpanName) + .Distinct() + .ToArrayAsync(); + return operations; + } + + public async Task FindTracesAsync(JaegerTraceQueryParameters query) + { + await using var context = await contextFactory.CreateDbContextAsync(); + var queryableSpans = context.Spans.AsQueryable(); + + if (!string.IsNullOrEmpty(query.ServiceName)) + { + queryableSpans = queryableSpans.Where(s => s.ServiceName == query.ServiceName); + } + + if (!string.IsNullOrEmpty(query.OperationName)) + { + queryableSpans = queryableSpans.Where(s => s.SpanName == query.OperationName); + } + + if (query.StartTimeMinUnixNano.HasValue) + { + queryableSpans = queryableSpans.Where(s => s.StartTimeUnixNano >= query.StartTimeMinUnixNano.Value); + } + + if (query.StartTimeMaxUnixNano.HasValue) + { + queryableSpans = queryableSpans.Where(s => s.StartTimeUnixNano <= query.StartTimeMaxUnixNano.Value); + } + + if (query.DurationMinNanoseconds.HasValue) + { + queryableSpans = + queryableSpans.Where(s => s.DurationNanoseconds >= query.DurationMinNanoseconds.Value); + } + + if (query.DurationMaxNanoseconds.HasValue) + { + queryableSpans = + queryableSpans.Where(s => s.DurationNanoseconds <= query.DurationMaxNanoseconds.Value); + } + + if (query.Tags?.Any() ?? false) + { + // TODO: This is a hacky way to do this, but it works for now. We should find a better way to match tags. + var tags = query.Tags.Select(tag => $"{tag.Key}:{tag.Value}").ToHashSet(); + var queryableAttributes = + context.SpanAttributes + .Where(a => tags.Contains(a.Key + ":" + a.Value)); + + var spanIds = queryableAttributes.GroupBy(a => a.SpanId) + .Where(a => a.Count() == query.Tags.Count()) + .Select(a => a.Key); + + queryableSpans = from span in queryableSpans + join spanId in spanIds on span.SpanId equals spanId + select span; + } + + if (query.NumTraces > 0) + { + queryableSpans = queryableSpans + .OrderByDescending(s => s.Id) + .Take(query.NumTraces); + } + + return await QueryJaegerTracesAsync(queryableSpans, context); + } + + public async Task FindTracesAsync( + string[]? traceIDs, + ulong? startTimeUnixNano, + ulong? endTimeUnixNano) + { + await using var context = await contextFactory.CreateDbContextAsync(); + + var queryableSpans = context.Spans.AsQueryable(); + + if (traceIDs?.Any() ?? false) + { + queryableSpans = queryableSpans.Where(s => traceIDs.Contains(s.TraceId)); + } + + if (startTimeUnixNano.HasValue) + { + queryableSpans = queryableSpans.Where(s => s.StartTimeUnixNano >= startTimeUnixNano.Value); + } + + if (endTimeUnixNano.HasValue) + { + queryableSpans = queryableSpans.Where(s => s.StartTimeUnixNano <= endTimeUnixNano.Value); + } + + return await QueryJaegerTracesAsync(queryableSpans, context); + } + + private static async Task QueryJaegerTracesAsync( + IQueryable queryableSpans, + MochaContext context) + { + var spans = await queryableSpans.ToArrayAsync(); + + var spanIds = spans.Select(s => s.SpanId).ToArray(); + + var spanAttributes = await context.SpanAttributes + .Where(a => spanIds.Contains(a.SpanId)) + .ToArrayAsync(); + + var resourceAttributes = await context.ResourceAttributes + .Where(a => spanIds.Contains(a.SpanId)) + .ToArrayAsync(); + + var spanEvents = await context.SpanEvents + .Where(e => spanIds.Contains(e.SpanId)) + .ToArrayAsync(); + + var spanEventAttributes = await context.SpanEventAttributes + .Where(a => spanIds.Contains(a.SpanId)) + .ToArrayAsync(); + + var jaegerTraces = spans.ToJaegerTraces( + spanAttributes, resourceAttributes, spanEvents, spanEventAttributes).ToArray(); + + return jaegerTraces; + } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Jaeger/EFToJaegerSpanConversionExtensions.cs b/src/Mocha.Storage/EntityFrameworkCore/Jaeger/EFToJaegerSpanConversionExtensions.cs new file mode 100644 index 0000000..9f363a6 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Jaeger/EFToJaegerSpanConversionExtensions.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Mocha.Core.Extensions; +using Mocha.Core.Storage.Jaeger.Trace; +using Mocha.Storage.EntityFrameworkCore.Trace; + +namespace Mocha.Storage.EntityFrameworkCore.Jaeger; + +internal static class EFToJaegerSpanConversionExtensions +{ + public static IEnumerable ToJaegerTraces( + this IEnumerable spans, + IEnumerable spanAttributes, + IEnumerable resourceAttributes, + IEnumerable spanEvents, + IEnumerable spanEventAttributes) + { + var spanAttributesBySpanId = spanAttributes + .GroupBy(a => a.SpanId) + .ToDictionary(g => g.Key, g => g.ToArray()); + + var resourceAttributesBySpanId = resourceAttributes + .GroupBy(a => a.SpanId) + .ToDictionary(g => g.Key, g => g.ToArray()); + + var spanEventsBySpanId = spanEvents + .GroupBy(e => e.SpanId) + .ToDictionary(g => g.Key, g => g.ToArray()); + + var spanEventAttributesBySpanId = spanEventAttributes + .GroupBy(a => a.SpanId) + .ToDictionary(g => g.Key, g => g.ToArray()); + + var jaegerSpans = new List(); + + foreach (var g in spans.GroupBy(s => s.SpanId)) + { + var spanId = g.Key; + var efSpans = g; + + spanAttributesBySpanId.TryGetValue(spanId, out var efSpanAttributes); + resourceAttributesBySpanId.TryGetValue(spanId, out var efResourceAttributes); + spanEventsBySpanId.TryGetValue(spanId, out var efSpanEvents); + spanEventAttributesBySpanId.TryGetValue(spanId, out var efSpanEventAttributes); + + efSpanAttributes ??= Array.Empty(); + efSpanEvents ??= Array.Empty(); + efSpanEventAttributes ??= Array.Empty(); + + jaegerSpans.AddRange(efSpans.ToJaegerSpans(efSpanAttributes, efSpanEvents, efSpanEventAttributes)); + } + + var jaegerTraces = jaegerSpans + .GroupBy(s => s.TraceID) + .Select(g => + { + var spansOfCurrentTrace = g.ToArray(); + var jaegerProcesses = new List(); + + foreach (var span in spansOfCurrentTrace) + { + resourceAttributesBySpanId.TryGetValue(span.SpanID, out var attributes); + attributes ??= Array.Empty(); + var process = new JaegerProcess + { + ProcessID = span.ProcessID, + ServiceName = attributes + .FirstOrDefault(a => a.Key == "service.name")?.Value ?? string.Empty, + Tags = Array.ConvertAll(attributes, ToJaegerTag) + }; + + jaegerProcesses.Add(process); + } + + return new JaegerTrace + { + TraceID = g.Key, + Processes = jaegerProcesses + .DistinctBy(p => p.ProcessID) + .ToDictionary(p => p.ProcessID), + Spans = spansOfCurrentTrace + }; + }); + + return jaegerTraces; + } + + private static IEnumerable ToJaegerSpans( + this IEnumerable spans, + IEnumerable spanAttributes, + IEnumerable spanEvents, + IEnumerable spanEventAttributes) + { + foreach (var span in spans) + { + var jaegerSpan = new JaegerSpan + { + TraceID = span.TraceId, + SpanID = span.SpanId, + OperationName = span.SpanName, + Flags = span.TraceFlags, // TODO: is this correct? + StartTime = span.StartTimeUnixNano / 1000, + Duration = span.DurationNanoseconds / 1000, + ProcessID = span.ServiceInstanceId, + References = string.IsNullOrWhiteSpace(span.ParentSpanId) // TODO: should we use span links? + ? Array.Empty() + : + [ + new JaegerSpanReference + { + TraceID = span.TraceId, + SpanID = span.ParentSpanId, + RefType = JaegerSpanReferenceType.ChildOf, + } + ], + Tags = spanAttributes.ToJaegerSpanTags(span).ToArray(), + Logs = spanEvents.ToJaegerSpanLogs(spanEventAttributes).ToArray() + }; + + yield return jaegerSpan; + } + } + + private static JaegerTag ToJaegerTag(this AbstractEFAttribute attribute) + { + var jaegerTag = new JaegerTag + { + Key = attribute.Key, + Type = attribute.ValueType.ToJaegerTagType(), + Value = ConvertTagValue(attribute.ValueType, attribute.Value) + }; + + return jaegerTag; + } + + private static IEnumerable ToJaegerSpanTags( + this IEnumerable spanAttributes, + EFSpan span) + { + if (span.StatusCode == EFSpanStatusCode.Error) + { + yield return new JaegerTag { Key = "error", Type = JaegerTagType.Bool, Value = true }; + } + + yield return new JaegerTag + { + Key = "span.kind", + Type = JaegerTagType.String, + Value = span.SpanKind.ToJaegerSpanKind() + }; + + foreach (var attribute in spanAttributes) + { + yield return attribute.ToJaegerTag(); + } + } + + private static IEnumerable ToJaegerSpanLogs( + this IEnumerable spanEvents, + IEnumerable spanEventAttributes) + { + var attributesBySpanEvent = spanEventAttributes + .GroupBy(a => a.SpanEventIndex) + .ToDictionary(g => g.Key, g => g.ToArray()); + + foreach (var spanEvent in spanEvents) + { + var jaegerSpanLog = new JaegerSpanLog + { + Timestamp = spanEvent.TimestampUnixNano / 1000, + Fields = attributesBySpanEvent.TryGetValue(spanEvent.Index, out var attributes) + ? attributes.Select(a => new JaegerTag + { + Key = a.Key, + Type = a.ValueType.ToJaegerTagType(), + Value = a.Value + }).ToArray() + : Array.Empty() + }; + + yield return jaegerSpanLog; + } + } + + private static string ToJaegerTagType(this EFAttributeValueType valueType) => valueType switch + { + EFAttributeValueType.StringValue => JaegerTagType.String, + EFAttributeValueType.BoolValue => JaegerTagType.Bool, + EFAttributeValueType.IntValue => JaegerTagType.Int64, + EFAttributeValueType.DoubleValue => JaegerTagType.Float64, + // TODO: ArrayValue, KvlistValue, BytesValue + EFAttributeValueType.ArrayValue => JaegerTagType.String, + EFAttributeValueType.KvlistValue => JaegerTagType.String, + EFAttributeValueType.BytesValue => JaegerTagType.String, + _ => throw new ArgumentOutOfRangeException() + }; + + private static string ToJaegerSpanKind(this EFSpanKind spanKind) => spanKind switch + { + EFSpanKind.Internal => JaegerSpanKind.Internal, + EFSpanKind.Server => JaegerSpanKind.Server, + EFSpanKind.Client => JaegerSpanKind.Client, + EFSpanKind.Producer => JaegerSpanKind.Producer, + EFSpanKind.Consumer => JaegerSpanKind.Consumer, + _ => JaegerSpanKind.Unspecified + }; + + private static object ConvertTagValue(this EFAttributeValueType valueType, string value) => valueType switch + { + EFAttributeValueType.StringValue => value, + EFAttributeValueType.BoolValue => bool.Parse(value), + EFAttributeValueType.IntValue => long.Parse(value), + EFAttributeValueType.DoubleValue => double.Parse(value), + // TODO: ArrayValue, KvlistValue, BytesValue + EFAttributeValueType.ArrayValue => value, + EFAttributeValueType.KvlistValue => value, + EFAttributeValueType.BytesValue => value, + _ => throw new ArgumentOutOfRangeException() + }; +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/MochaContext.cs b/src/Mocha.Storage/EntityFrameworkCore/MochaContext.cs index 41a1dc8..cb4f54f 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/MochaContext.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/MochaContext.cs @@ -3,23 +3,26 @@ using System.Reflection; using Microsoft.EntityFrameworkCore; +using Mocha.Core.Models.Trace; using Mocha.Storage.EntityFrameworkCore.Trace; namespace Mocha.Storage.EntityFrameworkCore; -public class MochaContext : DbContext +public class MochaContext(DbContextOptions options) : DbContext(options) { - public MochaContext(DbContextOptions options) : base(options) - { - } - - public DbSet SpanAttributes => Set(); + public DbSet Spans => Set(); public DbSet SpanEvents => Set(); public DbSet SpanLinks => Set(); - public DbSet Spans => Set(); + public DbSet SpanAttributes => Set(); + + public DbSet ResourceAttributes => Set(); + + public DbSet SpanEventAttributes => Set(); + + public DbSet SpanLinkAttributes => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/AbstractEFAttribute.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/AbstractEFAttribute.cs new file mode 100644 index 0000000..a67a931 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/AbstractEFAttribute.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +public abstract class AbstractEFAttribute +{ + public required string Key { get; init; } + + public required EFAttributeValueType ValueType { get; init; } + + public required string Value { get; init; } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFAttributeValueType.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFAttributeValueType.cs new file mode 100644 index 0000000..ddc5916 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFAttributeValueType.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +public enum EFAttributeValueType +{ + StringValue = 1, + BoolValue = 2, + IntValue = 3, + DoubleValue = 4, + ArrayValue = 5, + KvlistValue = 6, + BytesValue = 7, +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFResourceAttribute.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFResourceAttribute.cs new file mode 100644 index 0000000..94a9e85 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFResourceAttribute.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +public class EFResourceAttribute : AbstractEFAttribute +{ + public long Id { get; init; } + + public required string TraceId { get; init; } + + public required string SpanId { get; init; } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpan.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpan.cs index 4f79eee..5cce92a 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpan.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpan.cs @@ -1,43 +1,40 @@ // Licensed to the .NET Core Community under one or more agreements. // The .NET Core Community licenses this file to you under the MIT license. -using Mocha.Core.Enums; +using Mocha.Core.Models; +using Mocha.Core.Models.Trace; namespace Mocha.Storage.EntityFrameworkCore.Trace; public class EFSpan { - public long Id { get; set; } + public long Id { get; init; } - public string TraceId { get; set; } = string.Empty; + public required string TraceId { get; init; } - public string SpanId { get; set; } = string.Empty; + public required string SpanId { get; init; } - public string SpanName { get; set; } = string.Empty; + public required string SpanName { get; init; } - public string ParentSpanId { get; set; } = string.Empty; + public required string ParentSpanId { get; init; } - public string ServiceName { get; set; } = string.Empty; + public ulong StartTimeUnixNano { get; init; } - public long StartTime { get; set; } + public ulong EndTimeUnixNano { get; init; } - public long EndTime { get; set; } + public ulong DurationNanoseconds { get; init; } - public double Duration { get; set; } + public EFSpanStatusCode? StatusCode { get; init; } - public int StatusCode { get; set; } + public string? StatusMessage { get; init; } - public string? StatusMessage { get; set; } = string.Empty; + public EFSpanKind SpanKind { get; init; } - public SpanKind SpanKind { get; set; } + public required string ServiceName { get; init; } - public uint TraceFlags { get; set; } + public required string ServiceInstanceId { get; init; } - public string? TraceState { get; set; } + public uint TraceFlags { get; init; } - public ICollection SpanLinks { get; set; } = new List(); - - public ICollection SpanAttributes { get; set; } = new List(); - - public ICollection SpanEvents { get; set; } = new List(); + public string? TraceState { get; init; } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanAttribute.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanAttribute.cs index 77eb529..070e6fc 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanAttribute.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanAttribute.cs @@ -3,17 +3,11 @@ namespace Mocha.Storage.EntityFrameworkCore.Trace; -public class EFSpanAttribute +public class EFSpanAttribute : AbstractEFAttribute { - public long Id { get; set; } + public long Id { get; init; } - public string AttributeKey { get; set; } = string.Empty; + public required string TraceId { get; init; } - public string AttributeValue { get; set; } = string.Empty; - - public string TraceId { get; set; } = string.Empty; - - public string SpanId { get; set; } = string.Empty; - - public EFSpan Span { get; set; } = default!; + public required string SpanId { get; init; } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanEvent.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanEvent.cs index 25c0dde..e70cd95 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanEvent.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanEvent.cs @@ -6,13 +6,15 @@ namespace Mocha.Storage.EntityFrameworkCore.Trace; public class EFSpanEvent { - public long Id { get; set; } + public long Id { get; init; } - public string TraceId { get; set; } = string.Empty; + public required string TraceId { get; init; } - public long TimeBucket { get; set; } + public required string SpanId { get; init; } - public string EventName { get; set; } = string.Empty; + public int Index { get; init; } - public EFSpan Span { get; set; } = default!; + public required string Name { get; init; } + + public ulong TimestampUnixNano { get; init; } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanEventAttribute.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanEventAttribute.cs new file mode 100644 index 0000000..2bdb272 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanEventAttribute.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +public class EFSpanEventAttribute : AbstractEFAttribute +{ + public long Id { get; init; } + + public required string TraceId { get; init; } + + public required string SpanId { get; init; } + + public int SpanEventIndex { get; init; } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanKind.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanKind.cs new file mode 100644 index 0000000..331060f --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanKind.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +public enum EFSpanKind +{ + Unspecified = 0, + Internal = 1, + Server = 2, + Client = 3, + Producer = 4, + Consumer = 5 +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanLink.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanLink.cs index b36e281..605bcad 100644 --- a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanLink.cs +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanLink.cs @@ -5,17 +5,19 @@ namespace Mocha.Storage.EntityFrameworkCore.Trace; public class EFSpanLink { - public long Id { get; set; } + public long Id { get; init; } - public string TraceId { get; set; } = string.Empty; + public required string TraceId { get; init; } - public string SpanId { get; set; } = string.Empty; + public required string SpanId { get; init; } - public string LinkedSpanId { get; set; } = string.Empty; + public int Index { get; init; } - public string TraceState { get; set; } = string.Empty; + public required string LinkedTraceId { get; init; } - public uint Flags { get; set; } + public required string LinkedSpanId { get; init; } - public EFSpan Span { get; set; } = default!; + public required string LinkedTraceState { get; init; } + + public uint LinkedTraceFlags { get; init; } } diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanLinkAttribute.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanLinkAttribute.cs new file mode 100644 index 0000000..afb85d9 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanLinkAttribute.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +public class EFSpanLinkAttribute : AbstractEFAttribute +{ + public long Id { get; init; } + + public required string TraceId { get; init; } + + public required string SpanId { get; init; } + + public int SpanLinkIndex { get; init; } +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanStatusCode.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanStatusCode.cs new file mode 100644 index 0000000..0fa6625 --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/EFSpanStatusCode.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +public enum EFSpanStatusCode +{ + Unset = 0, + Ok = 1, + Error = 2, +} diff --git a/src/Mocha.Storage/EntityFrameworkCore/Trace/MochaToEFSpanConversionExtensions.cs b/src/Mocha.Storage/EntityFrameworkCore/Trace/MochaToEFSpanConversionExtensions.cs new file mode 100644 index 0000000..2298ced --- /dev/null +++ b/src/Mocha.Storage/EntityFrameworkCore/Trace/MochaToEFSpanConversionExtensions.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Mocha.Core.Models.Trace; + +namespace Mocha.Storage.EntityFrameworkCore.Trace; + +internal static class MochaToEFSpanConversionExtensions +{ + internal static EFSpan ToEFSpan(this MochaSpan span) + { + var efSpan = new EFSpan + { + TraceId = span.TraceId, + SpanId = span.SpanId, + SpanName = span.SpanName, + ParentSpanId = span.ParentSpanId, + StartTimeUnixNano = span.StartTimeUnixNano, + EndTimeUnixNano = span.EndTimeUnixNano, + DurationNanoseconds = span.DurationNanoseconds, + StatusCode = (EFSpanStatusCode?)span.StatusCode, + StatusMessage = span.StatusMessage, + SpanKind = (EFSpanKind)span.SpanKind, + ServiceName = span.Resource.ServiceName, + ServiceInstanceId = span.Resource.ServiceInstanceId, + TraceFlags = span.TraceFlags, + TraceState = span.TraceState + }; + + return efSpan; + } + + public static IEnumerable ToEFSpanAttributes(this MochaSpan span) + { + return span.Attributes.Select(a => new EFSpanAttribute + { + TraceId = span.TraceId, + SpanId = span.SpanId, + Key = a.Key, + ValueType = (EFAttributeValueType)a.ValueType, + Value = a.Value + }); + } + + public static IEnumerable ToEFResourceAttributes(this MochaSpan span) + { + return span.Resource.Attributes.Select(a => new EFResourceAttribute + { + TraceId = span.TraceId, + SpanId = span.SpanId, + Key = a.Key, + ValueType = (EFAttributeValueType)a.ValueType, + Value = a.Value + }); + } + + public static EFSpanEvent ToEFSpanEvent(this MochaSpanEvent spanEvent, MochaSpan span, int spanEventIndex) + { + var efSpanEvent = new EFSpanEvent + { + TraceId = span.TraceId, + SpanId = span.SpanId, + Index = spanEventIndex, + Name = spanEvent.Name, + TimestampUnixNano = spanEvent.TimestampUnixNano + }; + + return efSpanEvent; + } + + public static IEnumerable ToEFSpanEventAttributes( + this MochaSpanEvent spanEvent, + EFSpanEvent efSpanEvent) + { + return spanEvent.Attributes.Select(a => new EFSpanEventAttribute + { + TraceId = efSpanEvent.TraceId, + SpanId = efSpanEvent.SpanId, + SpanEventIndex = efSpanEvent.Index, + Key = a.Key, + ValueType = (EFAttributeValueType)a.ValueType, + Value = a.Value + }); + } + + public static EFSpanLink ToEFSpanLink(this MochaSpanLink link, MochaSpan span, int spanLinkIndex) + { + var efLink = new EFSpanLink + { + TraceId = span.TraceId, + SpanId = span.SpanId, + Index = spanLinkIndex, + LinkedTraceId = link.LinkedTraceId, + LinkedSpanId = link.LinkedSpanId, + LinkedTraceState = link.LinkedTraceState, + LinkedTraceFlags = link.LinkedTraceFlags + }; + + return efLink; + } + + public static IEnumerable ToEFSpanLinkAttributes( + this MochaSpanLink spanLink, + EFSpanLink efSpanLink) + { + return spanLink.Attributes.Select(a => new EFSpanLinkAttribute + { + TraceId = efSpanLink.TraceId, + SpanId = efSpanLink.SpanId, + SpanLinkIndex = efSpanLink.Index, + Key = a.Key, + ValueType = (EFAttributeValueType)a.ValueType, + Value = a.Value + }); + } +} diff --git a/src/Mocha.Storage/Mocha.Storage.csproj b/src/Mocha.Storage/Mocha.Storage.csproj index 2561e1e..597d099 100644 --- a/src/Mocha.Storage/Mocha.Storage.csproj +++ b/src/Mocha.Storage/Mocha.Storage.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 enable enable true @@ -9,8 +9,7 @@ - - - + + diff --git a/src/Mocha.Storage/SpanKindConversionExtensions.cs b/src/Mocha.Storage/SpanKindConversionExtensions.cs deleted file mode 100644 index 8027b71..0000000 --- a/src/Mocha.Storage/SpanKindConversionExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Core Community under one or more agreements. -// The .NET Core Community licenses this file to you under the MIT license. - -using Mocha.Core.Enums; -using OTelSpanKind = OpenTelemetry.Proto.Trace.V1.Span.Types.SpanKind; - -namespace Mocha.Storage; - -public static class SpanKindConversionExtensions -{ - internal static SpanKind ToMochaSpanKind(this OTelSpanKind spanKind) - { - return spanKind switch - { - OTelSpanKind.Unspecified => SpanKind.Unspecified, - OTelSpanKind.Internal => SpanKind.Internal, - OTelSpanKind.Server => SpanKind.Server, - OTelSpanKind.Client => SpanKind.Client, - OTelSpanKind.Producer => SpanKind.Producer, - OTelSpanKind.Consumer => SpanKind.Consumer, - _ => throw new ArgumentOutOfRangeException(nameof(spanKind), spanKind, null) - }; - } -} diff --git a/src/Mocha.Storage/StorageOptionsBuilder.cs b/src/Mocha.Storage/StorageOptionsBuilder.cs index 177c83e..aa66728 100644 --- a/src/Mocha.Storage/StorageOptionsBuilder.cs +++ b/src/Mocha.Storage/StorageOptionsBuilder.cs @@ -5,12 +5,7 @@ namespace Mocha.Storage; -public class StorageOptionsBuilder +public class StorageOptionsBuilder(IServiceCollection services) { - public StorageOptionsBuilder(IServiceCollection services) - { - Services = services; - } - - public IServiceCollection Services { get; } + public IServiceCollection Services { get; } = services; } diff --git a/src/Mocha.Streaming/Mocha.Streaming.csproj b/src/Mocha.Streaming/Mocha.Streaming.csproj index 83b633a..de3d74d 100644 --- a/src/Mocha.Streaming/Mocha.Streaming.csproj +++ b/src/Mocha.Streaming/Mocha.Streaming.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable Mocha.Aggregator diff --git a/tests/Mocha.Core.Benchmarks/MemoryBufferQueueConsumeBenchmark.cs b/tests/Mocha.Core.Benchmarks/MemoryBufferQueueConsumeBenchmark.cs index a7c6d77..b12d984 100644 --- a/tests/Mocha.Core.Benchmarks/MemoryBufferQueueConsumeBenchmark.cs +++ b/tests/Mocha.Core.Benchmarks/MemoryBufferQueueConsumeBenchmark.cs @@ -15,6 +15,7 @@ public class MemoryBufferQueueConsumeBenchmark private IEnumerable> _consumers = default!; [Params(4096, 8192)] public int MessageSize { get; set; } + [Params(1, 10, 100, 1000)] public int BatchSize { get; set; } [IterationSetup] public void Setup() @@ -30,7 +31,8 @@ public void Setup() } _consumers = _memoryBufferQueue!.CreateConsumers( - new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = true }, Environment.ProcessorCount); + new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = true, BatchSize = BatchSize, }, + Environment.ProcessorCount); } [Benchmark] @@ -53,7 +55,7 @@ public void BlockingCollection_Concurrent_Consuming() } [Benchmark] - public void MemoryBufferQueue_Concurrent_Producing_Partition_ProcessorCount() + public void MemoryBufferQueue_Concurrent_Consuming_Partition_ProcessorCount() { var countDownEvent = new CountdownEvent(MessageSize); @@ -61,9 +63,9 @@ public void MemoryBufferQueue_Concurrent_Producing_Partition_ProcessorCount() { _ = Task.Run(async () => { - await foreach (var _ in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - countDownEvent.Signal(); + countDownEvent.Signal(items.Count()); } }); } diff --git a/tests/Mocha.Core.Benchmarks/Mocha.Core.Benchmarks.csproj b/tests/Mocha.Core.Benchmarks/Mocha.Core.Benchmarks.csproj index 92c38d7..51f235d 100644 --- a/tests/Mocha.Core.Benchmarks/Mocha.Core.Benchmarks.csproj +++ b/tests/Mocha.Core.Benchmarks/Mocha.Core.Benchmarks.csproj @@ -2,13 +2,13 @@ Exe - net7.0 + net8.0 enable enable - + diff --git a/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionOffsetTests.cs b/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionOffsetTests.cs index 8cf870c..fb80314 100644 --- a/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionOffsetTests.cs +++ b/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionOffsetTests.cs @@ -84,9 +84,25 @@ public void ToUInt64() var offset1 = new MemoryBufferPartitionOffset(0, 1); var offset2 = new MemoryBufferPartitionOffset(0, ulong.MaxValue); var offset3 = new MemoryBufferPartitionOffset(1, 0); + var offset4 = new MemoryBufferPartitionOffset(0, 2); Assert.Equal(1UL, offset1.ToUInt64()); Assert.Equal(ulong.MaxValue, offset2.ToUInt64()); Assert.Throws(() => offset3.ToUInt64()); + Assert.Equal(2UL, (ulong)offset4); + } + + [Fact] + public void ToInt32() + { + var offset1 = new MemoryBufferPartitionOffset(0, 1); + var offset2 = new MemoryBufferPartitionOffset(0, int.MaxValue); + var offset3 = new MemoryBufferPartitionOffset(1, 0); + var offset4 = new MemoryBufferPartitionOffset(0, 2); + + Assert.Equal(1, offset1.ToInt32()); + Assert.Equal(int.MaxValue, offset2.ToInt32()); + Assert.Throws(() => offset3.ToInt32()); + Assert.Equal(2, (int)offset4); } } diff --git a/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionTests.cs b/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionTests.cs index a80d3be..74a71ad 100644 --- a/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionTests.cs +++ b/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferPartitionTests.cs @@ -8,6 +8,81 @@ namespace Mocha.Core.Tests.Buffer.Memory; public class MemoryBufferPartitionTests { + [Fact] + public void Enqueue_And_TryPull() + { + MemoryBufferPartition.SegmentLength = 2; + + var partition = new MemoryBufferPartition(); + + for (var i = 0; i < 12; i++) + { + partition.Enqueue(i); + } + + Assert.True(partition.TryPull("TestGroup", 4, out var items)); + Assert.Equal(new[] { 0, 1, 2, 3 }, items); + partition.Commit("TestGroup"); + + Assert.True(partition.TryPull("TestGroup", 3, out items)); + Assert.Equal(new[] { 4, 5, 6 }, items); + partition.Commit("TestGroup"); + + Assert.True(partition.TryPull("TestGroup", 2, out items)); + Assert.Equal(new[] { 7, 8 }, items); + partition.Commit("TestGroup"); + + Assert.True(partition.TryPull("TestGroup", 4, out items)); + Assert.Equal(new[] { 9, 10, 11 }, items); + partition.Commit("TestGroup"); + + Assert.False(partition.TryPull("TestGroup", 2, out _)); + + partition.Enqueue(12); + + Assert.True(partition.TryPull("TestGroup", 3, out items)); + Assert.Equal(new[] { 12 }, items); + } + + [Fact] + public void Repeatable_Pull_If_Not_Commit() + { + MemoryBufferPartition.SegmentLength = 2; + + var partition = new MemoryBufferPartition(); + + for (var i = 0; i < 11; i++) + { + partition.Enqueue(i); + } + + Assert.True(partition.TryPull("TestGroup", 4, out var items)); + Assert.Equal(new[] { 0, 1, 2, 3 }, items); + + Assert.True(partition.TryPull("TestGroup", 3, out items)); + Assert.Equal(new[] { 0, 1, 2 }, items); + + partition.Commit("TestGroup"); + + Assert.True(partition.TryPull("TestGroup", 3, out items)); + Assert.Equal(new[] { 3, 4, 5 }, items); + + Assert.True(partition.TryPull("TestGroup", 5, out items)); + Assert.Equal(new[] { 3, 4, 5, 6, 7 }, items); + + partition.Commit("TestGroup"); + + Assert.True(partition.TryPull("TestGroup", 6, out items)); + Assert.Equal(new[] { 8, 9, 10 }, items); + + Assert.True(partition.TryPull("TestGroup", 3, out items)); + Assert.Equal(new[] { 8, 9, 10 }, items); + + partition.Commit("TestGroup"); + + Assert.False(partition.TryPull("TestGroup", 2, out _)); + } + [Fact] public void Segment_Will_Be_Recycled_If_All_Consumers_Consumed_Single_Group() { @@ -22,10 +97,10 @@ public void Segment_Will_Be_Recycled_If_All_Consumers_Consumed_Single_Group() var segments1 = GetSegments(partition); - for (var i = 0; i < 6; i++) + for (var i = 0; i < 2; i++) { - partition.TryPull("TestGroup", out var item); - Assert.Equal(i, item); + Assert.True(partition.TryPull("TestGroup", 3, out var items)); + Assert.Equal(new[] { i * 3, i * 3 + 1, i * 3 + 2 }, items); partition.Commit("TestGroup"); } @@ -33,8 +108,8 @@ public void Segment_Will_Be_Recycled_If_All_Consumers_Consumed_Single_Group() for (var i = 0; i < 4; i++) { - partition.TryPull("TestGroup", out var item); - Assert.Equal(i + 6, item); + Assert.True(partition.TryPull("TestGroup", 1, out var items)); + Assert.Equal(i + 6, items.Single()); partition.Commit("TestGroup"); } @@ -60,13 +135,13 @@ public void Segment_Will_Be_Recycled_If_All_Consumers_Consumed_MultipleGroup() for (var i = 0; i < 3; i++) { - Assert.True(partition.TryPull("TestGroup1", out var item)); + Assert.True(partition.TryPull("TestGroup1", 1, out _)); partition.Commit("TestGroup1"); } - for (var i = 0; i < 6; i++) + for (var i = 0; i < 2; i++) { - Assert.True(partition.TryPull("TestGroup2", out var item)); + Assert.True(partition.TryPull("TestGroup2", 3, out _)); partition.Commit("TestGroup2"); } @@ -94,13 +169,13 @@ public void Segment_Will_Not_Be_Recycled_If_Not_All_Consumers_Consumed_MultipleG for (var i = 0; i < 3; i++) { - Assert.True(partition.TryPull("TestGroup1", out var item)); + Assert.True(partition.TryPull("TestGroup1", 1, out _)); partition.Commit("TestGroup1"); } for (var i = 0; i < 2; i++) { - Assert.True(partition.TryPull("TestGroup2", out var item)); + Assert.True(partition.TryPull("TestGroup2", 1, out _)); partition.Commit("TestGroup2"); } diff --git a/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferQueueTests.cs b/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferQueueTests.cs index a93f301..77216fb 100644 --- a/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferQueueTests.cs +++ b/tests/Mocha.Core.Tests/Buffer/Memory/MemoryBufferQueueTests.cs @@ -23,7 +23,12 @@ public async Task Produce_And_Consume() { var queue = new MemoryBufferQueue("test", 1); var producer = queue.CreateProducer(); - var consumer = queue.CreateConsumer(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false }); + var consumer = queue.CreateConsumer(new BufferConsumerOptions + { + GroupName = "TestGroup", + AutoCommit = false, + BatchSize = 2 + }); var expectedValues = new int[10]; for (var i = 0; i < 10; i++) @@ -33,9 +38,13 @@ public async Task Produce_And_Consume() } var index = 0; - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - Assert.Equal(expectedValues[index++], item); + foreach (var item in items) + { + Assert.Equal(expectedValues[index++], item); + } + var valueTask = consumer.CommitAsync(); if (!valueTask.IsCompletedSuccessfully) { @@ -54,7 +63,8 @@ public async Task Produce_And_Consume_AutoCommit() { var queue = new MemoryBufferQueue("test", 1); var producer = queue.CreateProducer(); - var consumer = queue.CreateConsumer(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = true }); + var consumer = queue.CreateConsumer( + new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = true, BatchSize = 2 }); var expectedValues = new int[10]; for (var i = 0; i < 10; i++) @@ -64,9 +74,13 @@ public async Task Produce_And_Consume_AutoCommit() } var index = 0; - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - Assert.Equal(expectedValues[index++], item); + foreach (var item in items) + { + Assert.Equal(expectedValues[index++], item); + } + if (index == 10) { break; @@ -79,7 +93,8 @@ public async Task Produce_And_Consume_With_Multiple_Partitions() { var queue = new MemoryBufferQueue("test", 2); var producer = queue.CreateProducer(); - var consumer = queue.CreateConsumer(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false }); + var consumer = queue.CreateConsumer( + new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false, BatchSize = 2 }); var expectedValues = new int[10]; for (var i = 0; i < 10; i++) @@ -88,21 +103,24 @@ public async Task Produce_And_Consume_With_Multiple_Partitions() expectedValues[i] = i; } - var index = 0; - await foreach (var item in consumer.ConsumeAsync()) + var consumedValues = new List(); + await foreach (var items in consumer.ConsumeAsync()) { - Assert.Equal(expectedValues[index++], item); - var valueTask = consumer.CommitAsync(); - if (!valueTask.IsCompletedSuccessfully) + consumedValues.AddRange(items); + + if (consumedValues.Count == 10) { - await valueTask.AsTask(); + break; } - if (index == 10) + var valueTask = consumer.CommitAsync(); + if (!valueTask.IsCompletedSuccessfully) { - break; + await valueTask.AsTask(); } } + + Assert.Equal(expectedValues, consumedValues.OrderBy(x => x)); } [Fact] @@ -111,22 +129,25 @@ public async Task Produce_And_Consume_With_Multiple_Consumers() var queue = new MemoryBufferQueue("test", 2); var producer = queue.CreateProducer(); var consumers = queue - .CreateConsumers(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false }, 2).ToList(); + .CreateConsumers(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false, BatchSize = 6 }, + 2).ToList(); var consumer1 = consumers[0]; var consumer2 = consumers[1]; - await producer.ProduceAsync(1); - await producer.ProduceAsync(2); + for (var i = 0; i < 10; i++) + { + await producer.ProduceAsync(i); + } - await foreach (var item in consumer1.ConsumeAsync()) + await foreach (var items in consumer1.ConsumeAsync()) { - Assert.Equal(1, item); + Assert.Equal(new[] { 0, 2, 4, 6, 8 }, items); break; } - await foreach (var item in consumer2.ConsumeAsync()) + await foreach (var items in consumer2.ConsumeAsync()) { - Assert.Equal(2, item); + Assert.Equal(new[] { 1, 3, 5, 7, 9 }, items); break; } } @@ -136,20 +157,23 @@ public async Task Offset_Will_Not_Change_If_Consumer_Not_Commit() { var queue = new MemoryBufferQueue("test", 1); var producer = queue.CreateProducer(); - var consumer = queue.CreateConsumer(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false }); + var consumer = queue.CreateConsumer( + new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false, BatchSize = 7 }); - await producer.ProduceAsync(1); - await producer.ProduceAsync(2); + for (var i = 0; i < 10; i++) + { + await producer.ProduceAsync(i); + } - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - Assert.Equal(1, item); + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5, 6 }, items); break; } - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - Assert.Equal(1, item); + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5, 6 }, items); break; } @@ -159,9 +183,9 @@ public async Task Offset_Will_Not_Change_If_Consumer_Not_Commit() await valueTask.AsTask(); } - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - Assert.Equal(2, item); + Assert.Equal(new[] { 7, 8, 9 }, items); break; } } @@ -175,9 +199,9 @@ public async Task Consumer_Will_Wait_Until_Produce() var task = Task.Run(async () => { - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - Assert.Equal(1, item); + Assert.Equal(1, items.Single()); break; } }); @@ -189,42 +213,6 @@ public async Task Consumer_Will_Wait_Until_Produce() await task; } - [Fact] - public async Task Retry_Consumption_If_No_Committed_Offset() - { - var queue = new MemoryBufferQueue("test", 1); - var producer = queue.CreateProducer(); - var consumer = queue.CreateConsumer(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = false }); - - await producer.ProduceAsync(1); - await producer.ProduceAsync(2); - - var index = 0; - await foreach (var item in consumer.ConsumeAsync()) - { - if (index < 9) - { - Assert.Equal(1, item); - } - else if (index == 9) - { - Assert.Equal(2, item); - break; - } - - if (index == 8) - { - var valueTask = consumer.CommitAsync(); - if (!valueTask.IsCompletedSuccessfully) - { - await valueTask.AsTask(); - } - } - - index++; - } - } - [Fact] public void Equal_Distribution_Load_Balancing_Strategy_For_Consumers() { @@ -288,9 +276,9 @@ public void Concurrent_Producer_Single_Partition() var consumer = queue.CreateConsumer(new BufferConsumerOptions { GroupName = "TestGroup", AutoCommit = true }); _ = Task.Run(async () => { - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - if (countDownEvent.Signal()) + if (countDownEvent.Signal(items.Count())) { break; } @@ -329,9 +317,9 @@ public void Concurrent_Producer_Multiple_Partition() var countDownEvent = new CountdownEvent(messageSize); _ = Task.Run(async () => { - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - if (countDownEvent.Signal()) + if (countDownEvent.Signal(items.Count())) { break; } @@ -360,10 +348,22 @@ public void Concurrent_Producer_Multiple_Partition() } [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - public void Concurrent_Consumer_Multiple_Groups(int groupNumber) + [InlineData(1, 1)] + [InlineData(1, 10)] + [InlineData(1, 100)] + [InlineData(1, 1000)] + [InlineData(1, 10000)] + [InlineData(2, 1)] + [InlineData(2, 10)] + [InlineData(2, 100)] + [InlineData(2, 1000)] + [InlineData(2, 10000)] + [InlineData(3, 1)] + [InlineData(3, 10)] + [InlineData(3, 100)] + [InlineData(3, 1000)] + [InlineData(3, 10000)] + public void Concurrent_Consumer_Multiple_Groups(int groupNumber, int batchSize) { var messageSize = MemoryBufferPartition.SegmentLength * 4; var partitionNumber = Environment.ProcessorCount * 2; @@ -377,7 +377,12 @@ public void Concurrent_Consumer_Multiple_Groups(int groupNumber) { var consumers = queue .CreateConsumers( - new BufferConsumerOptions { GroupName = "TestGroup" + (groupIndex + 1), AutoCommit = true }, + new BufferConsumerOptions + { + GroupName = "TestGroup" + (groupIndex + 1), + AutoCommit = true, + BatchSize = batchSize + }, consumerNumberPerGroup) .ToList(); @@ -385,9 +390,9 @@ public void Concurrent_Consumer_Multiple_Groups(int groupNumber) { _ = Task.Run(async () => { - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - if (countdownEvent.Signal()) + if (countdownEvent.Signal(items.Count())) { break; } @@ -439,9 +444,9 @@ public void Concurrent_Producer_And_Concurrent_Consumer_Multiple_Groups(int grou { _ = Task.Run(async () => { - await foreach (var item in consumer.ConsumeAsync()) + await foreach (var items in consumer.ConsumeAsync()) { - if (countdownEvent.Signal()) + if (countdownEvent.Signal(items.Count())) { break; } diff --git a/tests/Mocha.Core.Tests/Conversions/OTelToMochaSpanConversionTests.cs b/tests/Mocha.Core.Tests/Conversions/OTelToMochaSpanConversionTests.cs new file mode 100644 index 0000000..b2d4e39 --- /dev/null +++ b/tests/Mocha.Core.Tests/Conversions/OTelToMochaSpanConversionTests.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using System.Globalization; +using Google.Protobuf; +using Mocha.Core.Extensions; +using Mocha.Core.Models.Trace; +using OpenTelemetry.Proto.Common.V1; +using OpenTelemetry.Proto.Resource.V1; +using OpenTelemetry.Proto.Trace.V1; +using Status = OpenTelemetry.Proto.Trace.V1.Status; + +namespace Mocha.Core.Tests.Conversions; + +public class OTelToMochaSpanConversionTests +{ + [Fact] + public void ConvertSpan() + { + var now = DateTimeOffset.Now; + var resource = new Resource + { + Attributes = + { + new List + { + new() + { + Key = "service.name", Value = new AnyValue { StringValue = "TestServiceName" }, + }, + new() + { + Key = "service.instance.id", + Value = new AnyValue { StringValue = "TestServiceInstanceId" }, + }, + new() + { + Key = "service.version", + Value = new AnyValue { StringValue = "TestServiceVersion" }, + } + } + } + }; + + var oTelSpan = new Span + { + TraceId = ConvertTraceIdToByteString("5ae111ddc72d3fea3c6e4501961d8c8a"), + SpanId = ConvertSpanIdToByteString("b10497b337748713"), + TraceState = "TestTraceState", + ParentSpanId = ConvertSpanIdToByteString("ef5b92deb45ee2f9"), + Flags = 1, + Name = "TestSpan", + Kind = Span.Types.SpanKind.Server, + StartTimeUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(1).ToUnixTimeNanoseconds(), + Status = new Status { Code = Status.Types.StatusCode.Ok, Message = "TestStatusMessage" }, + Attributes = + { + new List + { + new() { Key = "SpanAttributeKey1", Value = new AnyValue { StringValue = "SpanAttributeValue1" } }, + new() { Key = "SpanAttributeKey2", Value = new AnyValue { BoolValue = true } }, + new() { Key = "SpanAttributeKey3", Value = new AnyValue { IntValue = 3 } }, + new() { Key = "SpanAttributeKey4", Value = new AnyValue { DoubleValue = 1.1 } } + } + }, + Events = + { + new List + { + new() + { + TimeUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds(), + Name = "TestEvent1", + Attributes = + { + new List + { + new() + { + Key = "EventAttributeKey1", + Value = new AnyValue { StringValue = "EventAttributeValue1" } + }, + new() { Key = "EventAttributeKey2", Value = new AnyValue { BoolValue = true } }, + new() { Key = "EventAttributeKey3", Value = new AnyValue { IntValue = 31 } }, + new() { Key = "EventAttributeKey4", Value = new AnyValue { DoubleValue = 11.1 } }, + } + } + } + } + }, + Links = + { + new Span.Types.Link + { + TraceId = ConvertTraceIdToByteString("7ae111ddc72d3fea3c6e4501961d8c8a"), + SpanId = ConvertSpanIdToByteString("a10497b337748713"), + TraceState = "TestTraceState", + Flags = 1, + Attributes = + { + new List + { + new() + { + Key = "LinkAttributeKey1", + Value = new AnyValue { StringValue = "LinkAttributeValue1" } + }, + } + } + } + }, + }; + + var mochaSpan = oTelSpan.ToMochaSpan(resource); + var mochaResource = mochaSpan.Resource; + var mochaSpanAttributes = mochaSpan.Attributes; + var mochaSpanEvents = mochaSpan.Events; + var mochaSpanLinks = mochaSpan.Links; + + var expectResource = new MochaResource + { + ServiceName = "TestServiceName", + ServiceInstanceId = "TestServiceInstanceId", + Attributes = new List + { + new() + { + Key = "service.version", + ValueType = MochaAttributeValueType.StringValue, + Value = "TestServiceVersion" + } + } + }; + + var expectSpanAttributes = new List + { + new() + { + Key = "SpanAttributeKey1", + ValueType = MochaAttributeValueType.StringValue, + Value = "SpanAttributeValue1" + }, + new() { Key = "SpanAttributeKey2", ValueType = MochaAttributeValueType.BoolValue, Value = "True" }, + new() { Key = "SpanAttributeKey3", ValueType = MochaAttributeValueType.IntValue, Value = "3" }, + new() { Key = "SpanAttributeKey4", ValueType = MochaAttributeValueType.DoubleValue, Value = "1.1" } + }; + + var expectSpanEvents = new List + { + new() + { + Name = "TestEvent1", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds(), + Attributes = new List + { + new() + { + Key = "EventAttributeKey1", + ValueType = MochaAttributeValueType.StringValue, + Value = "EventAttributeValue1" + }, + new() + { + Key = "EventAttributeKey2", + ValueType = MochaAttributeValueType.BoolValue, + Value = "True" + }, + new() + { + Key = "EventAttributeKey3", + ValueType = MochaAttributeValueType.IntValue, + Value = "31" + }, + new() + { + Key = "EventAttributeKey4", + ValueType = MochaAttributeValueType.DoubleValue, + Value = "11.1" + } + } + } + }; + + var expectSpanLinks = new List + { + new() + { + LinkedTraceId = "7ae111ddc72d3fea3c6e4501961d8c8a", + LinkedSpanId = "a10497b337748713", + LinkedTraceState = "TestTraceState", + LinkedTraceFlags = 1, + Attributes = new List + { + new() + { + Key = "LinkAttributeKey1", + ValueType = MochaAttributeValueType.StringValue, + Value = "LinkAttributeValue1" + } + } + } + }; + + Assert.Equivalent(expectResource, mochaResource); + Assert.Equivalent(expectSpanAttributes, mochaSpanAttributes); + Assert.Equivalent(expectSpanEvents, mochaSpanEvents); + Assert.Equivalent(expectSpanLinks, mochaSpanLinks); + Assert.Equivalent( + new MochaSpan + { + TraceId = "5ae111ddc72d3fea3c6e4501961d8c8a", + SpanId = "b10497b337748713", + SpanName = "TestSpan", + ParentSpanId = "ef5b92deb45ee2f9", + StartTimeUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(1).ToUnixTimeNanoseconds(), + DurationNanoseconds = + now.AddMinutes(1).ToUnixTimeNanoseconds() - now.AddMinutes(-1).ToUnixTimeNanoseconds(), + StatusCode = MochaSpanStatusCode.Ok, + StatusMessage = "TestStatusMessage", + SpanKind = MochaSpanKind.Server, + Resource = expectResource, + TraceFlags = 1, + TraceState = "TestTraceState", + Attributes = expectSpanAttributes, + Events = expectSpanEvents, + Links = expectSpanLinks + }, mochaSpan); + } + + private static ByteString ConvertSpanIdToByteString(string spanId) + { + if (string.IsNullOrWhiteSpace(spanId)) + { + return ByteString.Empty; + } + + var bytes = ConvertLongToBytes(long.Parse(spanId, NumberStyles.HexNumber)); + return ByteString.CopyFrom(bytes); + } + + private static ByteString ConvertTraceIdToByteString(string traceId) + { + if (string.IsNullOrWhiteSpace(traceId)) + { + return ByteString.Empty; + } + + var high = long.Parse(traceId[..16], NumberStyles.HexNumber); + var low = long.Parse(traceId[16..], NumberStyles.HexNumber); + var bytes = ConvertLongToBytes(high).Concat(ConvertLongToBytes(low)).ToArray(); + return ByteString.CopyFrom(bytes); + } + + private static byte[] ConvertLongToBytes(long value) => + BitConverter.IsLittleEndian + ? BitConverter.GetBytes(value).Reverse().ToArray() + : BitConverter.GetBytes(value); +} diff --git a/tests/Mocha.Core.Tests/Extensions/DateTimeOffsetExtensionsTests.cs b/tests/Mocha.Core.Tests/Extensions/DateTimeOffsetExtensionsTests.cs new file mode 100644 index 0000000..87c57c0 --- /dev/null +++ b/tests/Mocha.Core.Tests/Extensions/DateTimeOffsetExtensionsTests.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Mocha.Core.Extensions; + +namespace Mocha.Core.Tests.Extensions; + +public class DateTimeOffsetExtensionsTests +{ + [Fact] + public void ToUnixTimeNanoseconds_ReturnsCorrectValue() + { + var dateTimeOffset = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); + var expected = 1609459200000000000UL; + + var actual = dateTimeOffset.ToUnixTimeNanoseconds(); + + Assert.Equal(expected, actual); + } +} diff --git a/tests/Mocha.Core.Tests/Extensions/JsonExtensions.cs b/tests/Mocha.Core.Tests/Extensions/JsonExtensions.cs new file mode 100644 index 0000000..5e6973c --- /dev/null +++ b/tests/Mocha.Core.Tests/Extensions/JsonExtensions.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Mocha.Core.Extensions; + +namespace Mocha.Core.Tests.Extensions; + +public class JsonExtensions +{ + [Fact] + public void ToJson_ReturnsCorrectValue() + { + var expected = "{\"id\":1,\"name\":\"Test\"}"; + var obj = new Foo { Id = 1, Name = "Test" }; + + var actual = obj.ToJson(); + + Assert.Equal(expected, actual); + } + + [Fact] + public void FromJson_ReturnsCorrectValue() + { + var json = "{\"id\":1,\"name\":\"Test\"}"; + var expected = new Foo { Id = 1, Name = "Test" }; + + var actual = json.FromJson(); + + Assert.Equivalent(expected, actual); + } + + private class Foo + { + public int Id { get; set; } + public string? Name { get; set; } + } +} diff --git a/tests/Mocha.Core.Tests/Mocha.Core.Tests.csproj b/tests/Mocha.Core.Tests/Mocha.Core.Tests.csproj index 635f188..7b4cfd5 100644 --- a/tests/Mocha.Core.Tests/Mocha.Core.Tests.csproj +++ b/tests/Mocha.Core.Tests/Mocha.Core.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable @@ -10,14 +10,13 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -28,5 +27,4 @@ - diff --git a/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFJaegerSpanReaderTests.cs b/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFJaegerSpanReaderTests.cs new file mode 100644 index 0000000..5b79e17 --- /dev/null +++ b/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFJaegerSpanReaderTests.cs @@ -0,0 +1,374 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Mocha.Core.Extensions; +using Mocha.Core.Storage.Jaeger; +using Mocha.Core.Storage.Jaeger.Trace; +using Mocha.Storage.EntityFrameworkCore; +using Mocha.Storage.EntityFrameworkCore.Trace; + +namespace Mocha.Storage.Tests.EntityFrameworkCore; + +public class EFJaegerSpanReaderTests : IDisposable +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly IJaegerSpanReader _jaegerSpanReader; + private readonly ServiceProvider _serviceProvider; + + public EFJaegerSpanReaderTests() + { + var services = new ServiceCollection(); + services.AddStorage(builder => + { + builder.UseEntityFrameworkCore(options => { options.UseInMemoryDatabase(Guid.NewGuid().ToString()); }); + }); + _serviceProvider = services.BuildServiceProvider(); + _jaegerSpanReader = _serviceProvider.GetRequiredService(); + _dbContextFactory = _serviceProvider.GetRequiredService>(); + } + + [Fact] + public async Task GetServicesAsync() + { + var spans = new[] + { + new EFSpan + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanName = "SpanName1", + ParentSpanId = "ParentSpanId1", + StartTimeUnixNano = 0, + EndTimeUnixNano = 0, + DurationNanoseconds = 0, + StatusCode = null, + StatusMessage = null, + SpanKind = EFSpanKind.Unspecified, + ServiceName = "ServiceName1", + ServiceInstanceId = "ServiceInstanceId1", + TraceFlags = 0, + TraceState = "TraceState1" + }, + new EFSpan + { + TraceId = "TraceId2", + SpanId = "SpanId2", + SpanName = "SpanName2", + ParentSpanId = "ParentSpanId2", + StartTimeUnixNano = 0, + EndTimeUnixNano = 0, + DurationNanoseconds = 0, + StatusCode = null, + StatusMessage = null, + SpanKind = EFSpanKind.Unspecified, + ServiceName = "ServiceName2", + ServiceInstanceId = "ServiceInstanceId2", + TraceFlags = 0, + TraceState = "TraceState2" + } + }; + + await using var context = await _dbContextFactory.CreateDbContextAsync(); + await context.Spans.AddRangeAsync(spans); + await context.SaveChangesAsync(); + + var services = await _jaegerSpanReader.GetServicesAsync(); + Assert.Equal(new[] { "ServiceName1", "ServiceName2" }, services); + } + + [Fact] + public async Task GetOperationsAsync() + { + var spans = new[] + { + new EFSpan + { + Id = 1, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanName = "SpanName1", + ParentSpanId = "ParentSpanId1", + StartTimeUnixNano = 0, + EndTimeUnixNano = 0, + DurationNanoseconds = 0, + StatusCode = null, + StatusMessage = null, + SpanKind = EFSpanKind.Unspecified, + ServiceName = "ServiceName1", + ServiceInstanceId = "ServiceInstanceId1", + TraceFlags = 0, + TraceState = "TraceState1" + }, + new EFSpan + { + Id = 2, + TraceId = "TraceId2", + SpanId = "SpanId2", + SpanName = "SpanName2", + ParentSpanId = "ParentSpanId2", + StartTimeUnixNano = 0, + EndTimeUnixNano = 0, + DurationNanoseconds = 0, + StatusCode = null, + StatusMessage = null, + SpanKind = EFSpanKind.Unspecified, + ServiceName = "ServiceName2", + ServiceInstanceId = "ServiceInstanceId2", + TraceFlags = 0, + TraceState = "TraceState2" + } + }; + + await using var context = await _dbContextFactory.CreateDbContextAsync(); + await context.Spans.AddRangeAsync(spans); + await context.SaveChangesAsync(); + + var operations = await _jaegerSpanReader.GetOperationsAsync("ServiceName1"); + Assert.Equal(new[] { "SpanName1" }, operations); + } + + [Fact] + public async Task FindTracesAsync_JaegerTraceQueryParameters() + { + var now = DateTimeOffset.Now; + var efSpans = new List + { + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanName = "SpanName1", + ParentSpanId = "ParentSpanId1", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(1).ToUnixTimeNanoseconds(), + DurationNanoseconds = 60_000_000_000, + StatusCode = EFSpanStatusCode.Ok, + StatusMessage = "StatusMessage1", + SpanKind = EFSpanKind.Server, + ServiceName = "ServiceName1", + ServiceInstanceId = "ServiceInstanceId1", + TraceFlags = 1, + TraceState = "TraceState1", + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId2", + SpanName = "SpanName2", + ParentSpanId = "ParentSpanId2", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(2).ToUnixTimeNanoseconds(), + DurationNanoseconds = 120_000_000_000, + StatusCode = EFSpanStatusCode.Error, + StatusMessage = "StatusMessage2", + SpanKind = EFSpanKind.Client, + ServiceName = "ServiceName2", + ServiceInstanceId = "ServiceInstanceId2", + TraceFlags = 1, + TraceState = "TraceState2", + } + }; + + var efSpanAttributes = new List + { + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey1", + ValueType = EFAttributeValueType.StringValue, + Value = "SpanAttributeValue1" + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey2", + ValueType = EFAttributeValueType.BoolValue, + Value = "True" + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey3", + ValueType = EFAttributeValueType.IntValue, + Value = "31" + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey4", + ValueType = EFAttributeValueType.DoubleValue, + Value = "11.1" + } + }; + + var efSpanEvents = new List + { + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + Index = 0, + Name = "EventName1", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds() + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + Index = 1, + Name = "EventName2", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds() + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId2", + Index = 0, + Name = "EventName3", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds() + } + }; + + var efSpanEventAttributes = new List + { + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey1", + ValueType = EFAttributeValueType.StringValue, + Value = "EventAttributeValue1" + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey2", + ValueType = EFAttributeValueType.BoolValue, + Value = "True" + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey3", + ValueType = EFAttributeValueType.IntValue, + Value = "31" + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey4", + ValueType = EFAttributeValueType.DoubleValue, + Value = "11.1" + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 1, + Key = "EventAttributeKey1", + ValueType = EFAttributeValueType.StringValue, + Value = "EventAttributeValue1" + } + }; + + await using var context = await _dbContextFactory.CreateDbContextAsync(); + await context.Spans.AddRangeAsync(efSpans); + await context.SpanAttributes.AddRangeAsync(efSpanAttributes); + await context.SpanEvents.AddRangeAsync(efSpanEvents); + await context.SpanEventAttributes.AddRangeAsync(efSpanEventAttributes); + await context.SaveChangesAsync(); + + var queryParameters = new JaegerTraceQueryParameters + { + ServiceName = "ServiceName1", + OperationName = "SpanName1", + Tags = new Dictionary + { + { "SpanAttributeKey1", "SpanAttributeValue1" }, + { "SpanAttributeKey2", true }, + { "SpanAttributeKey3", 31 }, + { "SpanAttributeKey4", 11.1 } + }, + StartTimeMinUnixNano = now.AddMinutes(-2).ToUnixTimeNanoseconds(), + StartTimeMaxUnixNano = now.AddMinutes(2).ToUnixTimeNanoseconds(), + DurationMinNanoseconds = 60_000_000_000, + DurationMaxNanoseconds = 120_000_000_000, + NumTraces = 10 + }; + + var traces = await _jaegerSpanReader.FindTracesAsync(queryParameters); + Assert.Single(traces); + } + + [Fact] + public async Task FindTracesAsync_TraceID() + { + var now = DateTimeOffset.Now; + var efSpans = new List + { + new() + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanName = "SpanName1", + ParentSpanId = "ParentSpanId1", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(1).ToUnixTimeNanoseconds(), + DurationNanoseconds = 60_000_000_000, + StatusCode = EFSpanStatusCode.Ok, + StatusMessage = "StatusMessage1", + SpanKind = EFSpanKind.Server, + ServiceName = "ServiceName1", + ServiceInstanceId = "ServiceInstanceId1", + TraceFlags = 1, + TraceState = "TraceState1", + }, + new() + { + TraceId = "TraceId1", + SpanId = "SpanId2", + SpanName = "SpanName2", + ParentSpanId = "ParentSpanId2", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(2).ToUnixTimeNanoseconds(), + DurationNanoseconds = 120_000_000_000, + StatusCode = EFSpanStatusCode.Error, + StatusMessage = "StatusMessage2", + SpanKind = EFSpanKind.Client, + ServiceName = "ServiceName2", + ServiceInstanceId = "ServiceInstanceId2", + TraceFlags = 1, + TraceState = "TraceState2", + } + }; + + await using var context = await _dbContextFactory.CreateDbContextAsync(); + await context.Spans.AddRangeAsync(efSpans); + await context.SaveChangesAsync(); + + var traces = await _jaegerSpanReader.FindTracesAsync( + ["TraceId1"], + now.AddMinutes(-2).ToUnixTimeNanoseconds(), + now.AddMinutes(2).ToUnixTimeNanoseconds()); + Assert.Single(traces); + Assert.Equal("TraceId1", traces[0].TraceID); + } + + public void Dispose() + { + _serviceProvider.Dispose(); + } +} diff --git a/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFServiceCollectionExtensionsTests.cs b/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFServiceCollectionExtensionsTests.cs deleted file mode 100644 index 87429de..0000000 --- a/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFServiceCollectionExtensionsTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Core Community under one or more agreements. -// The .NET Core Community licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.DependencyInjection; -using Mocha.Storage.EntityFrameworkCore; - -namespace Mocha.Storage.Tests.EntityFrameworkCore; - -public class EFServiceCollectionExtensionsTests -{ - [Fact] - public void AddStorage() - { - var services = new ServiceCollection(); - services.AddStorage(x => - { - x.UseEntityFrameworkCore(context => - { - context.UseInMemoryDatabase($"InMemoryMochaContextTest{Guid.NewGuid().ToString()}") - .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)); - }); - }); - } -} diff --git a/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFSpanWriterTests.cs b/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFSpanWriterTests.cs new file mode 100644 index 0000000..220dc32 --- /dev/null +++ b/tests/Mocha.Storage.Tests/EntityFrameworkCore/EFSpanWriterTests.cs @@ -0,0 +1,518 @@ +// Licensed to the .NET Core Community under one or more agreements. +// The .NET Core Community licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Mocha.Core.Extensions; +using Mocha.Core.Models.Trace; +using Mocha.Core.Storage; +using Mocha.Storage.EntityFrameworkCore; +using Mocha.Storage.EntityFrameworkCore.Trace; + +namespace Mocha.Storage.Tests.EntityFrameworkCore; + +public class EFSpanWriterTests : IDisposable +{ + private readonly ServiceProvider _serviceProvider; + private readonly ISpanWriter _spanWriter; + + public EFSpanWriterTests() + { + var services = new ServiceCollection(); + services.AddStorage(builder => + { + builder.UseEntityFrameworkCore(options => + { + options.UseInMemoryDatabase(Guid.NewGuid().ToString()); + }); + }); + _serviceProvider = services.BuildServiceProvider(); + _spanWriter = _serviceProvider.GetRequiredService(); + } + + [Fact] + public async Task WriteSpanAsync() + { + var now = DateTimeOffset.Now; + var spans = new[] + { + new MochaSpan + { + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanName = "SpanName1", + ParentSpanId = "ParentSpanId1", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(1).ToUnixTimeNanoseconds(), + DurationNanoseconds = 60_000_000_000, + StatusCode = MochaSpanStatusCode.Ok, + StatusMessage = "StatusMessage1", + SpanKind = MochaSpanKind.Server, + Resource = + new MochaResource + { + ServiceName = "ServiceName1", + ServiceInstanceId = "ServiceInstanceId1", + Attributes = + new[] + { + new MochaAttribute + { + Key = "ServiceVersion", + ValueType = MochaAttributeValueType.StringValue, + Value = "ServiceVersion1" + } + } + }, + TraceFlags = 1, + TraceState = "TraceState1", + Attributes = + new[] + { + new MochaAttribute + { + Key = "SpanAttributeKey1", + ValueType = MochaAttributeValueType.StringValue, + Value = "SpanAttributeValue1" + }, + new MochaAttribute + { + Key = "SpanAttributeKey2", + ValueType = MochaAttributeValueType.BoolValue, + Value = "True" + }, + new MochaAttribute + { + Key = "SpanAttributeKey3", + ValueType = MochaAttributeValueType.IntValue, + Value = "31" + }, + new MochaAttribute + { + Key = "SpanAttributeKey4", + ValueType = MochaAttributeValueType.DoubleValue, + Value = "11.1" + } + }, + Events = new[] + { + new MochaSpanEvent + { + Name = "EventName1", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds(), + Attributes = + new[] + { + new MochaAttribute + { + Key = "EventAttributeKey1", + ValueType = MochaAttributeValueType.StringValue, + Value = "EventAttributeValue1" + }, + new MochaAttribute + { + Key = "EventAttributeKey2", + ValueType = MochaAttributeValueType.BoolValue, + Value = "True" + }, + new MochaAttribute + { + Key = "EventAttributeKey3", + ValueType = MochaAttributeValueType.IntValue, + Value = "31" + }, + new MochaAttribute + { + Key = "EventAttributeKey4", + ValueType = MochaAttributeValueType.DoubleValue, + Value = "11.1" + } + } + }, + new MochaSpanEvent + { + Name = "EventName2", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds(), + Attributes = new[] + { + new MochaAttribute + { + Key = "EventAttributeKey1", + ValueType = MochaAttributeValueType.StringValue, + Value = "EventAttributeValue1" + } + } + } + }, + Links = + new[] + { + new MochaSpanLink + { + LinkedTraceId = "LinkedTraceId1", + LinkedSpanId = "LinkedSpanId1", + LinkedTraceState = "LinkedTraceState1", + LinkedTraceFlags = 1, + Attributes = + new[] + { + new MochaAttribute + { + Key = "LinkAttributeKey1", + ValueType = MochaAttributeValueType.StringValue, + Value = "LinkAttributeValue1" + }, + new MochaAttribute + { + Key = "LinkAttributeKey2", + ValueType = MochaAttributeValueType.IntValue, + Value = "21" + }, + } + }, + new MochaSpanLink + { + LinkedTraceId = "LinkedTraceId2", + LinkedSpanId = "LinkedSpanId2", + LinkedTraceState = "LinkedTraceState2", + LinkedTraceFlags = 2, + Attributes = + new[] + { + new MochaAttribute + { + Key = "LinkAttributeKey3", + ValueType = MochaAttributeValueType.BoolValue, + Value = "True" + } + } + } + }, + }, + new MochaSpan + { + TraceId = "TraceId2", + SpanId = "SpanId2", + SpanName = "SpanName2", + ParentSpanId = "ParentSpanId2", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(2) + .ToUnixTimeNanoseconds(), + DurationNanoseconds = 120_000_000_000, + StatusCode = MochaSpanStatusCode.Error, + StatusMessage = "StatusMessage2", + SpanKind = MochaSpanKind.Client, + Resource = + new MochaResource + { + ServiceName = "ServiceName2", + ServiceInstanceId = "ServiceInstanceId2", + Attributes = + new[] + { + new MochaAttribute + { + Key = "ServiceVersion", + ValueType = MochaAttributeValueType.StringValue, + Value = "ServiceVersion2" + } + } + }, + TraceFlags = 1, + TraceState = "TraceState2", + Links = Enumerable.Empty(), + Attributes = Enumerable.Empty(), + Events = new[] + { + new MochaSpanEvent + { + Name = "EventName3", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds(), + Attributes = Enumerable.Empty() + } + } + } + }; + + await _spanWriter.WriteAsync(spans); + + var dbContextFactory = _serviceProvider.GetRequiredService>(); + await using var context = await dbContextFactory.CreateDbContextAsync(); + + var efSpans = context.Spans.ToList(); + var efResourceAttributes = context.ResourceAttributes.ToList(); + var efSpanAttributes = context.SpanAttributes.ToList(); + var efSpanEvents = context.SpanEvents.ToList(); + var efSpanLinks = context.SpanLinks.ToList(); + var efSpanEventAttributes = context.SpanEventAttributes.ToList(); + var efSpanLinkAttributes = context.SpanLinkAttributes.ToList(); + + var expectedSpans = new List + { + new() + { + Id = 1, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanName = "SpanName1", + ParentSpanId = "ParentSpanId1", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(1).ToUnixTimeNanoseconds(), + DurationNanoseconds = 60_000_000_000, + StatusCode = EFSpanStatusCode.Ok, + StatusMessage = "StatusMessage1", + SpanKind = EFSpanKind.Server, + ServiceName = "ServiceName1", + ServiceInstanceId = "ServiceInstanceId1", + TraceFlags = 1, + TraceState = "TraceState1", + }, + new() + { + Id = 2, + TraceId = "TraceId2", + SpanId = "SpanId2", + SpanName = "SpanName2", + ParentSpanId = "ParentSpanId2", + StartTimeUnixNano = now.ToUnixTimeNanoseconds(), + EndTimeUnixNano = now.AddMinutes(2).ToUnixTimeNanoseconds(), + DurationNanoseconds = 120_000_000_000, + StatusCode = EFSpanStatusCode.Error, + StatusMessage = "StatusMessage2", + SpanKind = EFSpanKind.Client, + ServiceName = "ServiceName2", + ServiceInstanceId = "ServiceInstanceId2", + TraceFlags = 1, + TraceState = "TraceState2", + } + }; + + var expectedSpanAttributes = new List + { + new() + { + Id = 1, + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey1", + ValueType = EFAttributeValueType.StringValue, + Value = "SpanAttributeValue1" + }, + new() + { + Id = 2, + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey2", + ValueType = EFAttributeValueType.BoolValue, + Value = "True" + }, + new() + { + Id = 3, + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey3", + ValueType = EFAttributeValueType.IntValue, + Value = "31" + }, + new() + { + Id = 4, + TraceId = "TraceId1", + SpanId = "SpanId1", + Key = "SpanAttributeKey4", + ValueType = EFAttributeValueType.DoubleValue, + Value = "11.1" + } + }; + + var expectedSpanEvents = new List + { + new() + { + Id = 1, + TraceId = "TraceId1", + SpanId = "SpanId1", + Index = 0, + Name = "EventName1", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds() + }, + new() + { + Id = 2, + TraceId = "TraceId1", + SpanId = "SpanId1", + Index = 1, + Name = "EventName2", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds() + }, + new() + { + Id = 3, + TraceId = "TraceId2", + SpanId = "SpanId2", + Index = 0, + Name = "EventName3", + TimestampUnixNano = now.AddMinutes(-1).ToUnixTimeNanoseconds() + } + }; + + var expectedSpanEventAttributes = new List + { + new() + { + Id = 1, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey1", + ValueType = EFAttributeValueType.StringValue, + Value = "EventAttributeValue1" + }, + new() + { + Id = 2, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey2", + ValueType = EFAttributeValueType.BoolValue, + Value = "True" + }, + new() + { + Id = 3, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey3", + ValueType = EFAttributeValueType.IntValue, + Value = "31" + }, + new() + { + Id = 4, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 0, + Key = "EventAttributeKey4", + ValueType = EFAttributeValueType.DoubleValue, + Value = "11.1" + }, + new() + { + Id = 5, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanEventIndex = 1, + Key = "EventAttributeKey1", + ValueType = EFAttributeValueType.StringValue, + Value = "EventAttributeValue1" + } + }; + + var expectedSpanLinks = new List + { + new() + { + Id = 1, + TraceId = "TraceId1", + SpanId = "SpanId1", + Index = 0, + LinkedTraceId = "LinkedTraceId1", + LinkedSpanId = "LinkedSpanId1", + LinkedTraceState = "LinkedTraceState1", + LinkedTraceFlags = 1 + }, + new() + { + Id = 2, + TraceId = "TraceId1", + SpanId = "SpanId1", + Index = 1, + LinkedTraceId = "LinkedTraceId2", + LinkedSpanId = "LinkedSpanId2", + LinkedTraceState = "LinkedTraceState2", + LinkedTraceFlags = 2 + } + }; + + var expectedSpanLinkAttributes = new List + { + new() + { + Id = 1, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanLinkIndex = 0, + Key = "LinkAttributeKey1", + ValueType = EFAttributeValueType.StringValue, + Value = "LinkAttributeValue1" + }, + new() + { + Id = 2, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanLinkIndex = 0, + Key = "LinkAttributeKey2", + ValueType = EFAttributeValueType.IntValue, + Value = "21" + }, + new() + { + Id = 3, + TraceId = "TraceId1", + SpanId = "SpanId1", + SpanLinkIndex = 1, + Key = "LinkAttributeKey3", + ValueType = EFAttributeValueType.BoolValue, + Value = "True" + } + }; + + Assert.Equal(2, efSpans.Count); + Assert.Equal(2, efResourceAttributes.Count); + Assert.Equal(4, efSpanAttributes.Count); + Assert.Equal(3, efSpanEvents.Count); + Assert.Equal(2, efSpanLinks.Count); + Assert.Equal(5, efSpanEventAttributes.Count); + Assert.Equal(3, efSpanLinkAttributes.Count); + + for (var i = 0; i < efSpans.Count; i++) + { + Assert.Equivalent(expectedSpans[i], efSpans[i]); + } + + for (var i = 0; i < efSpanAttributes.Count; i++) + { + Assert.Equivalent(expectedSpanAttributes[i], efSpanAttributes[i]); + } + + for (var i = 0; i < efSpanEvents.Count; i++) + { + Assert.Equivalent(expectedSpanEvents[i], efSpanEvents[i]); + } + + for (var i = 0; i < efSpanEventAttributes.Count; i++) + { + Assert.Equivalent(expectedSpanEventAttributes[i], efSpanEventAttributes[i]); + } + + for (var i = 0; i < efSpanLinks.Count; i++) + { + Assert.Equivalent(expectedSpanLinks[i], efSpanLinks[i]); + } + + for (var i = 0; i < efSpanLinkAttributes.Count; i++) + { + Assert.Equivalent(expectedSpanLinkAttributes[i], efSpanLinkAttributes[i]); + } + } + + public void Dispose() + { + _serviceProvider.Dispose(); + } +} diff --git a/tests/Mocha.Storage.Tests/EntityFrameworkCore/InMemoryMochaContextTest.cs b/tests/Mocha.Storage.Tests/EntityFrameworkCore/InMemoryMochaContextTest.cs deleted file mode 100644 index 96bced4..0000000 --- a/tests/Mocha.Storage.Tests/EntityFrameworkCore/InMemoryMochaContextTest.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Licensed to the .NET Core Community under one or more agreements. -// The .NET Core Community licenses this file to you under the MIT license. - -using System.Diagnostics; -using Google.Protobuf; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.DependencyInjection; -using Mocha.Core.Storage; -using Mocha.Storage.EntityFrameworkCore; -using OpenTelemetry.Proto.Common.V1; -using OpenTelemetry.Proto.Trace.V1; -using Span = OpenTelemetry.Proto.Trace.V1.Span; - -namespace Mocha.Storage.Tests.EntityFrameworkCore; - -public class InMemoryMochaContextTest -{ - private readonly DbContextOptions _contextOptions; - private readonly IServiceCollection _serviceCollection; - - public InMemoryMochaContextTest() - { - _serviceCollection = new ServiceCollection(); - _contextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("InMemoryMochaContextTest") - .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)) - .Options; - _serviceCollection.AddStorage(x => - { - x.UseEntityFrameworkCore(context => - { - context.UseInMemoryDatabase($"InMemoryMochaContextTest{Guid.NewGuid().ToString()}") - .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)); - }); - }); - } - - [Fact] - public async Task CreateDatabase() - { - await using var context = new MochaContext(_contextOptions); - await context.Database.EnsureDeletedAsync(); - await context.Database.EnsureCreatedAsync(); - } - - - [Fact] - public async Task EntityFrameworkSpanWriterAsync() - { - var provider = _serviceCollection.BuildServiceProvider(); - var context = provider.GetRequiredService(); - await context.Database.EnsureDeletedAsync(); - await context.Database.EnsureCreatedAsync(); - var entityFrameworkSpanWriter = provider.GetRequiredService(); - var traceId = ActivityTraceId.CreateRandom(); - var spanId = ActivitySpanId.CreateRandom(); - var traceIdBytes = new byte[16]; - var spanIdBytes = new byte[8]; - traceId.CopyTo(traceIdBytes); - spanId.CopyTo(spanIdBytes); - ByteString parentSpanIdString; - var parentSpanIdBytes = new byte[8]; - ActivitySpanId.CreateRandom().CopyTo(parentSpanIdBytes); - parentSpanIdString = UnsafeByteOperations.UnsafeWrap(parentSpanIdBytes); - - var span = new Span() - { - Name = "Http", - Kind = Span.Types.SpanKind.Client, - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - ParentSpanId = parentSpanIdString, - TraceState = "string.Empty", - StartTimeUnixNano = (ulong)DateTimeOffset.UtcNow.UtcTicks, - EndTimeUnixNano = (ulong)DateTimeOffset.UtcNow.UtcTicks, - Status = new Status() { Message = "Success", Code = Status.Types.StatusCode.Ok, }, - }; - span.Links.Add(new Span.Types.Link() - { - TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes), - SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes), - TraceState = "", - Flags = 1, - }); - span.Events.Add( - new Span.Types.Event() { Name = "mysql", TimeUnixNano = (ulong)DateTimeOffset.UtcNow.UtcTicks, }); - span.Attributes.Add(new KeyValue() - { - Key = "http.url", - Value = new AnyValue() { StringValue = "https://github.com/open-telemetry/opentelemetry-dotnet" } - }); - - var spans = new List() { span }; - await entityFrameworkSpanWriter.WriteAsync(spans); - } -} diff --git a/tests/Mocha.Storage.Tests/Usings.cs b/tests/Mocha.Storage.Tests/GlobalUsings.cs similarity index 100% rename from tests/Mocha.Storage.Tests/Usings.cs rename to tests/Mocha.Storage.Tests/GlobalUsings.cs diff --git a/tests/Mocha.Storage.Tests/Mocha.Storage.Tests.csproj b/tests/Mocha.Storage.Tests/Mocha.Storage.Tests.csproj index 8e65bde..36407b6 100644 --- a/tests/Mocha.Storage.Tests/Mocha.Storage.Tests.csproj +++ b/tests/Mocha.Storage.Tests/Mocha.Storage.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable @@ -10,14 +10,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all