Compare commits

...

9 Commits

Author SHA1 Message Date
srikanthccv
303d6a0363 feat: add semconv migration product surfaces 2026-08-07 01:21:14 +05:30
srikanthccv
6b4bb82efb feat: resolve semconv families across logs and metrics 2026-08-07 01:20:58 +05:30
srikanthccv
487d00b6c9 test: add semantic convention phase one closure gate 2026-08-07 01:20:55 +05:30
srikanthccv
59329d3aaa feat: support semconv evolution in services 2026-08-07 01:20:49 +05:30
srikanthccv
7c58b6af49 feat: resolve semantic convention names in trace queries 2026-08-07 01:20:08 +05:30
srikanthccv
e730192885 feat: generate semantic convention families 2026-08-07 01:20:01 +05:30
Jugal Kishore
8a481d8a75 feat(onboarding): add Apache, HAProxy, Elasticsearch, RabbitMQ, Open WebUI, DSPy datasources (#12363)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
2026-08-06 15:28:41 +00:00
Shivam Gupta
84eeacd084 feat(onboarding): add HCP Vault, OpenTelemetry eBPF, Langflow, Cohere, Auth0, and PgBouncer datasources (#12098)
* feat(onboarding): add HCP Vault, OpenTelemetry eBPF, Langflow, Cohere, and Auth0 datasources

Add UI onboarding configurations (logos + datasource entries) so these tools
appear in the Add Data Source onboarding flow. SVG logos optimized with svgo.

Closes signoz.io#3614 (HCP Vault)
Closes signoz.io#3626 (OpenTelemetry eBPF / OBI)
Closes signoz.io#3680 (Langflow)
Closes signoz.io#3702 (Cohere)
Closes signoz.io#3562 (Auth0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(onboarding): add PgBouncer datasource

Add PgBouncer (PostgreSQL connection pooler) metrics onboarding entry,
reusing the existing PostgreSQL logo.

Closes signoz.io#3553 (PgBouncer)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(onboarding): update SVG logos and category of datasources

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jugal Kishore <jugal@signoz.io>
2026-08-06 13:25:53 +00:00
Vinicius Lourenço
41576954c4 fix(query-context): ensure operator is correctly suggested (#11575)
Co-authored-by: Nikhil Mantri <nikhil.mantri1999@gmail.com>
2026-08-06 13:24:06 +00:00
122 changed files with 6146 additions and 335 deletions

View File

@@ -53,6 +53,21 @@ jobs:
with:
PRIMUS_REF: main
GO_VERSION: 1.24
semconv-generated:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: check-semconv-generated-files
run: go run ./scripts/semconv -check
build:
if: |
github.event_name == 'merge_group' ||

View File

@@ -220,6 +220,18 @@ py-test-teardown: ## Tear down the shared SigNoz backend
py-test: ## Runs integration tests
@cd tests && uv run pytest --basetemp=./tmp/ -vv --capture=no integration/tests/
.PHONY: py-test-semconv-phase1
py-test-semconv-phase1: py-test-setup ## Rebuild the shared stack and run the semantic-convention Phase 1 matrix
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/tests/queriertraces/13_semconv_evolution.py
.PHONY: py-test-semconv-phase2
py-test-semconv-phase2: py-test-setup ## Rebuild the shared stack and run the Phase 1-2 cross-signal matrices
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/tests/queriertraces/13_semconv_evolution.py integration/tests/queriersemconv/02_cross_signal.py
.PHONY: py-test-semconv-phase3
py-test-semconv-phase3: py-test-setup ## Rebuild the shared stack and run the Phase 1-3 compatibility and migration-report matrices
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/tests/queriertraces/13_semconv_evolution.py integration/tests/queriersemconv/02_cross_signal.py
.PHONY: py-clean
py-clean: ## Clear all pycache and pytest cache from tests directory recursively
@echo ">> cleaning python cache files from tests directory"
@@ -233,6 +245,10 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
##############################################################
# generate commands
##############################################################
.PHONY: semconv-generate
semconv-generate: ## Regenerate semantic-convention families for Go and TypeScript
@go run ./scripts/semconv
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"

View File

@@ -0,0 +1,9 @@
import axios from 'api';
import { SemconvMigrationReport } from 'types/api/semconvMigration';
async function getSemconvMigrationReport(): Promise<SemconvMigrationReport> {
const response = await axios.get('/fields/semconv-migration');
return response.data.data;
}
export default getSemconvMigrationReport;

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" fill-rule="evenodd" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>AWS</title><path d="M6.763 11.212q.002.446.088.71c.064.176.144.368.256.576.04.063.056.127.056.183q.002.12-.152.24l-.503.335a.4.4 0 0 1-.208.072q-.12-.002-.239-.112a2.5 2.5 0 0 1-.287-.375 6 6 0 0 1-.248-.471q-.934 1.101-2.347 1.101c-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583q-.001-.908-.375-1.277c-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103s-.583.16-.862.272a2 2 0 0 1-.28.104.5.5 0 0 1-.127.023q-.168.002-.168-.247v-.391c0-.128.016-.224.056-.28a.6.6 0 0 1 .224-.167 4.6 4.6 0 0 1 1.005-.36 4.8 4.8 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647q.661.646.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144a1.8 1.8 0 0 0 .758-.51 1.3 1.3 0 0 0 .272-.512c.047-.191.08-.423.08-.694v-.335a7 7 0 0 0-.735-.136 6 6 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296m6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.4 1.4 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783q.227-.001.31.08c.065.048.113.16.16.312l1.342 5.284 1.245-5.284q.058-.24.151-.312a.55.55 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348q.074-.24.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1 1 0 0 1-.056.2l-1.923 6.17q-.072.24-.168.311a.5.5 0 0 1-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.6.6 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247q.072 0 .144.024c.048.016.12.048.2.08q.408.181.878.279c.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.78.78 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.9 1.9 0 0 1-.4-1.158q0-.502.216-.886c.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088q.24.058.455.127.216.072.336.144a.7.7 0 0 1 .24.2.43.43 0 0 1 .071.263v.375q-.002.254-.184.256a.8.8 0 0 1-.303-.096 3.65 3.65 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223s-.375.383-.375.71c0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767s.367.702.367 1.117c0 .343-.072.655-.207.926a2.2 2.2 0 0 1-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167"/><path fill="#f90" d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351m23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="#9CA3AF" fill-rule="evenodd" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>AWS</title><path d="M6.763 11.212q.002.446.088.71c.064.176.144.368.256.576.04.063.056.127.056.183q.002.12-.152.24l-.503.335a.4.4 0 0 1-.208.072q-.12-.002-.239-.112a2.5 2.5 0 0 1-.287-.375 6 6 0 0 1-.248-.471q-.934 1.101-2.347 1.101c-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583q-.001-.908-.375-1.277c-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103s-.583.16-.862.272a2 2 0 0 1-.28.104.5.5 0 0 1-.127.023q-.168.002-.168-.247v-.391c0-.128.016-.224.056-.28a.6.6 0 0 1 .224-.167 4.6 4.6 0 0 1 1.005-.36 4.8 4.8 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647q.661.646.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144a1.8 1.8 0 0 0 .758-.51 1.3 1.3 0 0 0 .272-.512c.047-.191.08-.423.08-.694v-.335a7 7 0 0 0-.735-.136 6 6 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296m6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.4 1.4 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783q.227-.001.31.08c.065.048.113.16.16.312l1.342 5.284 1.245-5.284q.058-.24.151-.312a.55.55 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348q.074-.24.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1 1 0 0 1-.056.2l-1.923 6.17q-.072.24-.168.311a.5.5 0 0 1-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.6.6 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247q.072 0 .144.024c.048.016.12.048.2.08q.408.181.878.279c.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.78.78 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.9 1.9 0 0 1-.4-1.158q0-.502.216-.886c.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088q.24.058.455.127.216.072.336.144a.7.7 0 0 1 .24.2.43.43 0 0 1 .071.263v.375q-.002.254-.184.256a.8.8 0 0 1-.303-.096 3.65 3.65 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223s-.375.383-.375.71c0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767s.367.702.367 1.117c0 .343-.072.655-.207.926a2.2 2.2 0 0 1-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167"/><path fill="#f90" d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351m23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399"/></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,3 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M8.932 20.806c-.369 0-.738.007-1.109 0-.35-.007-.587-.206-.623-.5a.587.587 0 0 1 .53-.636c.79-.062 1.582-.063 2.372-.003a.548.548 0 0 1 .522.602c-.024.326-.253.526-.616.54zM1.792 8.345c-.392 0-.782.008-1.173.002-.327-.006-.577-.22-.614-.512-.037-.293.146-.544.499-.615.192-.032.388-.045.583-.039a81.515 81.515 0 0 1 1.597 0c.163 0 .325.019.483.056.288.073.445.318.411.617-.034.298-.214.477-.515.487-.424.014-.848.004-1.272.004zm7.588 8.417H4.292a2.464 2.464 0 0 1-.326-.007c-.294-.04-.48-.209-.508-.506-.029-.298.11-.501.391-.606.179-.065.365-.051.549-.051 3.347 0 6.695.005 10.042-.006 1.174-.004 2.187-.439 2.993-1.3.69-.738 1.053-1.63 1.16-2.635.085-.788-.027-1.513-.516-2.156-.544-.718-1.28-1.078-2.163-1.082-3.163-.013-6.328-.005-9.487-.01-.336 0-.673-.027-1.007-.058-.29-.027-.45-.201-.469-.492-.021-.317.141-.545.429-.6a1.55 1.55 0 0 1 .29-.015h10.177c1.71.004 3.187 1.038 3.726 2.654.383 1.147.246 2.304-.182 3.416-.824 2.135-2.762 3.448-5.055 3.454-1.652.005-3.304 0-4.956 0zm2.906-13.568c1.533 0 3.066-.008 4.598 0 2.935.018 5.629 1.892 6.653 4.626.442 1.181.538 2.403.412 3.657-.185 1.842-.735 3.552-1.776 5.084-1.608 2.365-3.873 3.68-6.679 4.118-.95.148-1.905.13-2.86.13-.397 0-.61-.181-.633-.51-.025-.351.196-.621.587-.645.434-.026.87-.004 1.305-.016 2.641-.072 4.928-.982 6.74-2.935 1.269-1.37 1.912-3.039 2.13-4.878.151-1.275.135-2.544-.37-3.752-.773-1.85-2.159-2.983-4.068-3.509-.74-.204-1.5-.243-2.26-.247-2.837-.017-5.675-.007-8.511-.007-.12 0-.24.004-.359-.006a.57.57 0 0 1-.517-.536.557.557 0 0 1 .456-.557c.13-.018.261-.024.392-.019h4.762Z"/>
<path fill="#29F1FB" d="M8.932 20.806c-.369 0-.738.007-1.109 0-.35-.007-.587-.206-.623-.5a.587.587 0 0 1 .53-.636c.79-.062 1.582-.063 2.372-.003a.548.548 0 0 1 .522.602c-.024.326-.253.526-.616.54zM1.792 8.345c-.392 0-.782.008-1.173.002-.327-.006-.577-.22-.614-.512-.037-.293.146-.544.499-.615.192-.032.388-.045.583-.039a81.515 81.515 0 0 1 1.597 0c.163 0 .325.019.483.056.288.073.445.318.411.617-.034.298-.214.477-.515.487-.424.014-.848.004-1.272.004zm7.588 8.417H4.292a2.464 2.464 0 0 1-.326-.007c-.294-.04-.48-.209-.508-.506-.029-.298.11-.501.391-.606.179-.065.365-.051.549-.051 3.347 0 6.695.005 10.042-.006 1.174-.004 2.187-.439 2.993-1.3.69-.738 1.053-1.63 1.16-2.635.085-.788-.027-1.513-.516-2.156-.544-.718-1.28-1.078-2.163-1.082-3.163-.013-6.328-.005-9.487-.01-.336 0-.673-.027-1.007-.058-.29-.027-.45-.201-.469-.492-.021-.317.141-.545.429-.6a1.55 1.55 0 0 1 .29-.015h10.177c1.71.004 3.187 1.038 3.726 2.654.383 1.147.246 2.304-.182 3.416-.824 2.135-2.762 3.448-5.055 3.454-1.652.005-3.304 0-4.956 0zm2.906-13.568c1.533 0 3.066-.008 4.598 0 2.935.018 5.629 1.892 6.653 4.626.442 1.181.538 2.403.412 3.657-.185 1.842-.735 3.552-1.776 5.084-1.608 2.365-3.873 3.68-6.679 4.118-.95.148-1.905.13-2.86.13-.397 0-.61-.181-.633-.51-.025-.351.196-.621.587-.645.434-.026.87-.004 1.305-.016 2.641-.072 4.928-.982 6.74-2.935 1.269-1.37 1.912-3.039 2.13-4.878.151-1.275.135-2.544-.37-3.752-.773-1.85-2.159-2.983-4.068-3.509-.74-.204-1.5-.243-2.26-.247-2.837-.017-5.675-.007-8.511-.007-.12 0-.24.004-.359-.006a.57.57 0 0 1-.517-.536.557.557 0 0 1 .456-.557c.13-.018.261-.024.392-.019h4.762Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#D22128" d="M17.805 2.197v.066h.156v.44h.072v-.44h.156v-.066zm.9 0l-.175.353-.172-.353h-.087v.506h.067V2.3l.172.35h.045l.172-.35v.404h.066v-.506zm-4.257 1c-.204.31-.424.66-.66 1.06l-.04.062a44.457 44.457 0 00-1.265 2.29c-.187.36-.38.742-.577 1.146l2.267-.25c.66-.302.955-.578 1.242-.976a15.5 15.5 0 00.23-.342c.23-.363.46-.763.663-1.16.197-.386.37-.767.505-1.11.083-.22.15-.422.198-.6.042-.158.074-.307.1-.45-.884.15-1.965.295-2.668.33zM11.894 7.78l-.077.16c-.078.16-.157.32-.236.488-.086.18-.172.364-.26.552l-.132.287a75.265 75.265 0 00-1.427 3.3c-.163.397-.327.807-.493 1.23-.15.38-.297.765-.45 1.164l-.02.06c-.15.396-.3.802-.453 1.22l-.01.027.72-.08a.213.213 0 01-.042-.006c.863-.106 2.01-.75 2.75-1.547.342-.367.652-.8.94-1.306.213-.377.413-.795.604-1.258.168-.405.328-.843.48-1.318-.196.105-.423.18-.673.235a2.184 2.184 0 01-.273.046c.806-.31 1.314-.905 1.683-1.64a2.816 2.816 0 01-.968.428c-.06.012-.116.022-.174.03l-.043.006h.002c.278-.118.514-.248.718-.403a2.571 2.571 0 00.637-.698l.063-.104.077-.154a8.107 8.107 0 00.367-.85l.03-.088a3.04 3.04 0 00.123-.463.733.733 0 01-.094.065c-.243.145-.66.277-.996.34l.663-.074-.664.073h-.017l-.1.017c.006-.003.01-.006.017-.008l-2.265.25-.013.022zM8.27 16.45c-.117.323-.236.654-.355.992l-.005.015c-.016.046-.032.094-.05.142-.08.227-.15.432-.31.9.264.12.475.435.675.793a1.44 1.44 0 00-.466-.99c1.293.06 2.41-.27 2.99-1.217.05-.084.096-.173.14-.268-.26.333-.59.474-1.2.44 0 0-.004 0-.005.002l.004-.002c.9-.404 1.354-.79 1.754-1.433.094-.153.186-.32.28-.503-.788.81-1.702 1.04-2.664.865l-.72.078a6.43 6.43 0 00-.067.183zM15.42.112c-.376.222-1 .85-1.748 1.763l.686 1.294c.48-.687.97-1.307 1.462-1.836l.058-.062c-.02.02-.04.04-.057.062-.16.176-.644.74-1.375 1.863.703-.035 1.784-.18 2.666-.33.262-1.47-.258-2.142-.258-2.142s-.66-1.07-1.436-.61zm-3.084 6.402a40.253 40.253 0 011.306-2.26l.04-.064c.224-.352.45-.693.677-1.02l-.685-1.293-.157.192c-.197.245-.403.51-.613.79a39.853 39.853 0 00-2.016 2.97l-.022.038.893 1.763c.19-.378.38-.752.575-1.118zm-3.73 8.32c.158-.406.319-.81.483-1.225.156-.394.32-.79.484-1.19a91.133 91.133 0 011.6-3.604l.205-.424c.12-.243.237-.485.36-.724a.125.125 0 01.02-.04l-.895-1.763-.044.07c-.207.34-.414.687-.617 1.042a38.056 38.056 0 00-1.092 2.04l-.094.193a24.573 24.573 0 00-1.258 3.087 18.492 18.492 0 00-.52 1.997l.896 1.77c.117-.317.24-.638.364-.963zm-1.376-.476a13.38 13.38 0 00-.234 1.692c0 .02-.004.04-.005.06-.28-.45-1.03-.888-1.026-.884.537.778.944 1.55 1.005 2.31-.29.058-.684-.027-1.14-.195.475.436.83.556.97.588-.434.03-.89.328-1.346.67.668-.27 1.21-.38 1.596-.29-.61 1.74-1.23 3.655-1.843 5.69a.538.538 0 00.364-.354c.11-.368.84-2.786 1.978-5.965l.097-.27.028-.078c.12-.332.246-.672.374-1.02l.09-.237v-.004L7.24 14.3c-.003.02-.01.04-.012.06z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#EB5424" d="M21.98 7.448 19.62 0H4.347L2.02 7.448c-1.352 4.312.03 9.206 3.815 12.015L12.007 24l6.157-4.552c3.755-2.81 5.182-7.688 3.815-12.015l-6.16 4.58 2.343 7.45-6.157-4.597-6.158 4.58 2.358-7.433-6.188-4.55 7.63-.045L12.008 0l2.356 7.404 7.615.044z"/></svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="34" viewBox="0 0 131 34">
<path fill="currentColor" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35ZM44.41 14.7c-.5-.5-1.1-.9-1.76-1.18a5.62 5.62 0 0 0-4.6.17c-.73.37-1.32.91-1.75 1.62h-.17V8.59H34.1v16.83h2.04v-1.81h.17c.21.36.47.67.77.94.31.25.65.48 1.01.67.37.18.77.31 1.18.39a6.2 6.2 0 0 0 3.39-.24 5.36 5.36 0 0 0 3.02-3.1c.29-.75.44-1.62.44-2.6v-.47c0-.96-.16-1.83-.47-2.58-.3-.75-.7-1.4-1.23-1.9v-.01Zm-5.87.66a3.9 3.9 0 0 1 4.34.84c.36.35.64.8.83 1.3.2.5.3 1.07.3 1.7v.47c0 .64-.1 1.23-.3 1.74a3.75 3.75 0 0 1-2.06 2.15 4.27 4.27 0 0 1-3.12-.03 3.86 3.86 0 0 1-2.09-2.2c-.2-.52-.3-1.11-.3-1.75v-.29c0-.62.1-1.2.3-1.7v-.01c.21-.53.5-.99.84-1.36.36-.37.78-.66 1.26-.86ZM97.04 8.59H95v4.86h-2.94v1.86H95v8.17c0 .56.17 1.03.53 1.4.37.35.84.54 1.4.54h4.18v-1.87h-3.5c-.2 0-.33-.05-.43-.15-.1-.1-.14-.27-.14-.49v-7.6h4.65v-1.86h-4.65V8.59ZM114.61 15a5.48 5.48 0 0 0-1.8-1.33 5.6 5.6 0 0 0-2.57-.56 6.17 6.17 0 0 0-4.26 1.7 5.6 5.6 0 0 0-1.72 4.2v.57c0 .9.15 1.75.44 2.5a5.58 5.58 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.35 5.35 0 0 0 1.91-2.73l.03-.07-1.94-.52-.02.07c-.11.33-.27.64-.46.94-.17.27-.4.52-.7.74-.28.22-.63.39-1.04.51-.41.13-.9.19-1.46.19a3.8 3.8 0 0 1-2.84-1.05 4.07 4.07 0 0 1-1.1-2.7h9.68v-1.6c0-.54-.11-1.12-.34-1.75a5.04 5.04 0 0 0-1.03-1.74Zm-8.25 3.21a3.8 3.8 0 0 1 1.22-2.25 4.19 4.19 0 0 1 3.99-.7c.44.16.83.38 1.17.66.34.27.62.62.82 1.02.21.38.34.8.38 1.27h-7.58ZM129.09 14.42a4.47 4.47 0 0 0-3.37-1.3c-.93 0-1.73.2-2.4.59-.64.39-1.15.97-1.52 1.74h-.17v-2h-2.04v11.97h2.04v-6.23c0-1.26.32-2.28.95-3.02a3.31 3.31 0 0 1 2.65-1.14c.94 0 1.7.3 2.24.9.56.6.83 1.52.83 2.74v6.75h2.04v-7.13c0-1.71-.42-3.02-1.25-3.87ZM88.1 15a5.48 5.48 0 0 0-1.78-1.33 5.6 5.6 0 0 0-2.58-.56 6.17 6.17 0 0 0-4.27 1.7 5.59 5.59 0 0 0-1.71 4.2v.56c0 .92.14 1.76.44 2.51a5.6 5.6 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.36 5.36 0 0 0 1.91-2.73l.03-.07-1.94-.52-.03.07c-.1.32-.26.64-.45.94-.17.27-.4.52-.7.74-.29.21-.64.39-1.05.51-.4.12-.9.19-1.45.19a3.8 3.8 0 0 1-2.85-1.05 4.07 4.07 0 0 1-1.09-2.7h9.68v-1.61c0-.53-.12-1.12-.34-1.74A5.03 5.03 0 0 0 88.1 15Zm-8.24 3.21a3.83 3.83 0 0 1 1.22-2.25 4.2 4.2 0 0 1 3.99-.7c.44.16.83.38 1.16.66.35.27.62.62.83 1.02.2.38.33.8.37 1.27h-7.57ZM73.65 19.42a6.11 6.11 0 0 0-3.23-1.02 6.63 6.63 0 0 1-2.68-.58c-.47-.3-.7-.7-.7-1.25 0-.27.08-.5.21-.7.14-.2.33-.38.56-.52a4.05 4.05 0 0 1 1.78-.42c.85 0 1.54.21 2.06.63.53.41.83 1 .91 1.73l.01.1 1.95-.47-.01-.07c-.07-.45-.22-.91-.45-1.36a3.46 3.46 0 0 0-.94-1.2 4.6 4.6 0 0 0-1.52-.84 6.05 6.05 0 0 0-2.1-.34c-.58 0-1.13.07-1.65.22-.52.14-1 .36-1.43.65-.41.3-.74.66-.99 1.1a2.9 2.9 0 0 0-.37 1.49v.14c0 1.06.38 1.87 1.14 2.42a6.2 6.2 0 0 0 3.24.97c1.18.08 2.04.26 2.56.54.5.28.75.72.75 1.36 0 .6-.25 1.06-.78 1.4-.52.32-1.22.48-2.1.48a3.68 3.68 0 0 1-2.46-.79 3.13 3.13 0 0 1-1.04-2.14v-.09l-1.93.46h-.02v.07a4.4 4.4 0 0 0 3.1 4c.7.24 1.52.36 2.46.36.7 0 1.35-.09 1.93-.27.6-.16 1.12-.4 1.53-.72A3.38 3.38 0 0 0 74.8 22v-.14c0-1.07-.39-1.9-1.14-2.44ZM60.25 23.4c-.1-.1-.14-.27-.14-.49v-9.46h-2.05v1.85h-.16a3.78 3.78 0 0 0-1.61-1.63 4.62 4.62 0 0 0-2.26-.56c-.77 0-1.5.14-2.19.41a5.27 5.27 0 0 0-3.02 3.12c-.29.75-.44 1.63-.44 2.6v.38c0 .99.15 1.87.44 2.63.3.75.7 1.4 1.2 1.93a5.48 5.48 0 0 0 4.05 1.57c.8 0 1.51-.2 2.2-.58.69-.38 1.24-.97 1.63-1.75h.16v.06c0 .56.18 1.03.53 1.4.37.35.85.54 1.41.54h1.36v-1.87h-.68c-.2 0-.34-.05-.43-.15Zm-4.46.13c-.46.2-.97.3-1.52.3a3.68 3.68 0 0 1-2.75-1.09 4.42 4.42 0 0 1-1.05-3.12v-.38c0-.62.1-1.2.29-1.71a3.65 3.65 0 0 1 5-2.17c.47.2.88.49 1.21.86.35.37.62.83.8 1.36.2.5.3 1.08.3 1.7v.3c0 .63-.1 1.23-.3 1.76-.18.5-.45.96-.78 1.33-.33.37-.73.66-1.2.86Z"/>
<svg xmlns="http://www.w3.org/2000/svg" fill="#9CA3AF" height="34" viewBox="0 0 131 34">
<path fill="#9CA3AF" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35ZM44.41 14.7c-.5-.5-1.1-.9-1.76-1.18a5.62 5.62 0 0 0-4.6.17c-.73.37-1.32.91-1.75 1.62h-.17V8.59H34.1v16.83h2.04v-1.81h.17c.21.36.47.67.77.94.31.25.65.48 1.01.67.37.18.77.31 1.18.39a6.2 6.2 0 0 0 3.39-.24 5.36 5.36 0 0 0 3.02-3.1c.29-.75.44-1.62.44-2.6v-.47c0-.96-.16-1.83-.47-2.58-.3-.75-.7-1.4-1.23-1.9v-.01Zm-5.87.66a3.9 3.9 0 0 1 4.34.84c.36.35.64.8.83 1.3.2.5.3 1.07.3 1.7v.47c0 .64-.1 1.23-.3 1.74a3.75 3.75 0 0 1-2.06 2.15 4.27 4.27 0 0 1-3.12-.03 3.86 3.86 0 0 1-2.09-2.2c-.2-.52-.3-1.11-.3-1.75v-.29c0-.62.1-1.2.3-1.7v-.01c.21-.53.5-.99.84-1.36.36-.37.78-.66 1.26-.86ZM97.04 8.59H95v4.86h-2.94v1.86H95v8.17c0 .56.17 1.03.53 1.4.37.35.84.54 1.4.54h4.18v-1.87h-3.5c-.2 0-.33-.05-.43-.15-.1-.1-.14-.27-.14-.49v-7.6h4.65v-1.86h-4.65V8.59ZM114.61 15a5.48 5.48 0 0 0-1.8-1.33 5.6 5.6 0 0 0-2.57-.56 6.17 6.17 0 0 0-4.26 1.7 5.6 5.6 0 0 0-1.72 4.2v.57c0 .9.15 1.75.44 2.5a5.58 5.58 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.35 5.35 0 0 0 1.91-2.73l.03-.07-1.94-.52-.02.07c-.11.33-.27.64-.46.94-.17.27-.4.52-.7.74-.28.22-.63.39-1.04.51-.41.13-.9.19-1.46.19a3.8 3.8 0 0 1-2.84-1.05 4.07 4.07 0 0 1-1.1-2.7h9.68v-1.6c0-.54-.11-1.12-.34-1.75a5.04 5.04 0 0 0-1.03-1.74Zm-8.25 3.21a3.8 3.8 0 0 1 1.22-2.25 4.19 4.19 0 0 1 3.99-.7c.44.16.83.38 1.17.66.34.27.62.62.82 1.02.21.38.34.8.38 1.27h-7.58ZM129.09 14.42a4.47 4.47 0 0 0-3.37-1.3c-.93 0-1.73.2-2.4.59-.64.39-1.15.97-1.52 1.74h-.17v-2h-2.04v11.97h2.04v-6.23c0-1.26.32-2.28.95-3.02a3.31 3.31 0 0 1 2.65-1.14c.94 0 1.7.3 2.24.9.56.6.83 1.52.83 2.74v6.75h2.04v-7.13c0-1.71-.42-3.02-1.25-3.87ZM88.1 15a5.48 5.48 0 0 0-1.78-1.33 5.6 5.6 0 0 0-2.58-.56 6.17 6.17 0 0 0-4.27 1.7 5.59 5.59 0 0 0-1.71 4.2v.56c0 .92.14 1.76.44 2.51a5.6 5.6 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.36 5.36 0 0 0 1.91-2.73l.03-.07-1.94-.52-.03.07c-.1.32-.26.64-.45.94-.17.27-.4.52-.7.74-.29.21-.64.39-1.05.51-.4.12-.9.19-1.45.19a3.8 3.8 0 0 1-2.85-1.05 4.07 4.07 0 0 1-1.09-2.7h9.68v-1.61c0-.53-.12-1.12-.34-1.74A5.03 5.03 0 0 0 88.1 15Zm-8.24 3.21a3.83 3.83 0 0 1 1.22-2.25 4.2 4.2 0 0 1 3.99-.7c.44.16.83.38 1.16.66.35.27.62.62.83 1.02.2.38.33.8.37 1.27h-7.57ZM73.65 19.42a6.11 6.11 0 0 0-3.23-1.02 6.63 6.63 0 0 1-2.68-.58c-.47-.3-.7-.7-.7-1.25 0-.27.08-.5.21-.7.14-.2.33-.38.56-.52a4.05 4.05 0 0 1 1.78-.42c.85 0 1.54.21 2.06.63.53.41.83 1 .91 1.73l.01.1 1.95-.47-.01-.07c-.07-.45-.22-.91-.45-1.36a3.46 3.46 0 0 0-.94-1.2 4.6 4.6 0 0 0-1.52-.84 6.05 6.05 0 0 0-2.1-.34c-.58 0-1.13.07-1.65.22-.52.14-1 .36-1.43.65-.41.3-.74.66-.99 1.1a2.9 2.9 0 0 0-.37 1.49v.14c0 1.06.38 1.87 1.14 2.42a6.2 6.2 0 0 0 3.24.97c1.18.08 2.04.26 2.56.54.5.28.75.72.75 1.36 0 .6-.25 1.06-.78 1.4-.52.32-1.22.48-2.1.48a3.68 3.68 0 0 1-2.46-.79 3.13 3.13 0 0 1-1.04-2.14v-.09l-1.93.46h-.02v.07a4.4 4.4 0 0 0 3.1 4c.7.24 1.52.36 2.46.36.7 0 1.35-.09 1.93-.27.6-.16 1.12-.4 1.53-.72A3.38 3.38 0 0 0 74.8 22v-.14c0-1.07-.39-1.9-1.14-2.44ZM60.25 23.4c-.1-.1-.14-.27-.14-.49v-9.46h-2.05v1.85h-.16a3.78 3.78 0 0 0-1.61-1.63 4.62 4.62 0 0 0-2.26-.56c-.77 0-1.5.14-2.19.41a5.27 5.27 0 0 0-3.02 3.12c-.29.75-.44 1.63-.44 2.6v.38c0 .99.15 1.87.44 2.63.3.75.7 1.4 1.2 1.93a5.48 5.48 0 0 0 4.05 1.57c.8 0 1.51-.2 2.2-.58.69-.38 1.24-.97 1.63-1.75h.16v.06c0 .56.18 1.03.53 1.4.37.35.85.54 1.41.54h1.36v-1.87h-.68c-.2 0-.34-.05-.43-.15Zm-4.46.13c-.46.2-.97.3-1.52.3a3.68 3.68 0 0 1-2.75-1.09 4.42 4.42 0 0 1-1.05-3.12v-.38c0-.62.1-1.2.29-1.71a3.65 3.65 0 0 1 5-2.17c.47.2.88.49 1.21.86.35.37.62.83.8 1.36.2.5.3 1.08.3 1.7v.3c0 .63-.1 1.23-.3 1.76-.18.5-.45.96-.78 1.33-.33.37-.73.66-1.2.86Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#355146" fill-rule="evenodd" d="M6.82 11.908c.525 0 1.57-.03 3.013-.639 1.682-.71 5.029-2 7.443-3.323 1.689-.926 2.429-2.151 2.429-3.8 0-2.29-1.81-4.146-4.043-4.146H6.307C3.1 0 .5 2.666.5 5.954s2.434 5.954 6.32 5.954" clip-rule="evenodd"/><path fill="#d18ee2" fill-rule="evenodd" d="M8.402 16.01c0-1.611.947-3.064 2.399-3.682l2.946-1.254c2.98-1.268 6.26.977 6.26 4.286 0 2.563-2.027 4.64-4.527 4.64l-3.19-.002c-2.147 0-3.888-1.785-3.888-3.987" clip-rule="evenodd"/><path fill="#ff7759" d="M3.848 12.691C1.998 12.691.5 14.228.5 16.124v.444C.5 18.464 1.999 20 3.848 20s3.347-1.536 3.347-3.432v-.444c0-1.896-1.499-3.433-3.347-3.433"/></svg>

After

Width:  |  Height:  |  Size: 709 B

View File

@@ -1,9 +1,9 @@
<svg width="109" height="24" viewBox="0 0 109 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_125_22125)">
<path d="M0 -2.08616e-07V24H17.9352C22.9911 24 26.0999 21.0858 26.0999 17.04V6.96C26.0999 2.91432 22.9911 -2.08616e-07 17.9352 -2.08616e-07H0ZM6.76413 5.82864H19.1992V18.1714H6.76413V5.82864Z" fill="currentColor"/>
<path d="M46.7659 18.6172H35.0824V14.16H46.7659V18.6172ZM46.595 5.38296V9.5658H35.0824V5.38296H46.595ZM50.2846 12.1373V11.5886C52.5734 10.5258 53.7008 8.8458 53.7008 6.13728C53.7008 2.64012 50.9337 0.000116183 45.5361 0.000116183H28.3184V24H45.7752C51.1728 24 53.9399 21.8401 53.9399 18.0685C53.9399 15.0172 52.6418 13.2001 50.2846 12.1373Z" fill="currentColor"/>
<path d="M62.397 18.1714H74.8319V5.82864H62.397V18.1714ZM63.6609 24C58.6049 24 55.4961 21.0858 55.4961 17.04V6.96012C55.4961 2.91432 58.6049 0.000116183 63.6609 0.000116183H73.568C78.6238 0.000116183 81.7326 2.91432 81.7326 6.96012V17.04C81.7326 21.0858 78.6238 24 73.568 24H63.6609Z" fill="currentColor"/>
<path d="M101.66 15.12L90.8995 14.3658C85.5361 13.9886 83.418 11.1772 83.418 7.47432V6.96012C83.418 2.91432 86.5266 0.000116183 91.5827 0.000116183H100.157C105.214 0.000116183 108.323 2.91432 108.323 6.96012V7.98864H101.968V5.14284H90.2504V8.43432L100.601 9.18864C105.999 9.56568 108.493 12.8572 108.493 16.5257V17.04C108.493 20.7428 105.384 24 100.328 24H91.5827C86.5266 24 83.418 20.7428 83.418 17.04V16.0115H89.7722V18.8572H101.66V15.12Z" fill="currentColor"/>
<path d="M0 -2.08616e-07V24H17.9352C22.9911 24 26.0999 21.0858 26.0999 17.04V6.96C26.0999 2.91432 22.9911 -2.08616e-07 17.9352 -2.08616e-07H0ZM6.76413 5.82864H19.1992V18.1714H6.76413V5.82864Z" fill="#9CA3AF"/>
<path d="M46.7659 18.6172H35.0824V14.16H46.7659V18.6172ZM46.595 5.38296V9.5658H35.0824V5.38296H46.595ZM50.2846 12.1373V11.5886C52.5734 10.5258 53.7008 8.8458 53.7008 6.13728C53.7008 2.64012 50.9337 0.000116183 45.5361 0.000116183H28.3184V24H45.7752C51.1728 24 53.9399 21.8401 53.9399 18.0685C53.9399 15.0172 52.6418 13.2001 50.2846 12.1373Z" fill="#9CA3AF"/>
<path d="M62.397 18.1714H74.8319V5.82864H62.397V18.1714ZM63.6609 24C58.6049 24 55.4961 21.0858 55.4961 17.04V6.96012C55.4961 2.91432 58.6049 0.000116183 63.6609 0.000116183H73.568C78.6238 0.000116183 81.7326 2.91432 81.7326 6.96012V17.04C81.7326 21.0858 78.6238 24 73.568 24H63.6609Z" fill="#9CA3AF"/>
<path d="M101.66 15.12L90.8995 14.3658C85.5361 13.9886 83.418 11.1772 83.418 7.47432V6.96012C83.418 2.91432 86.5266 0.000116183 91.5827 0.000116183H100.157C105.214 0.000116183 108.323 2.91432 108.323 6.96012V7.98864H101.968V5.14284H90.2504V8.43432L100.601 9.18864C105.999 9.56568 108.493 12.8572 108.493 16.5257V17.04C108.493 20.7428 105.384 24 100.328 24H91.5827C86.5266 24 83.418 20.7428 83.418 17.04V16.0115H89.7722V18.8572H101.66V15.12Z" fill="#9CA3AF"/>
</g>
<defs>
<clipPath id="clip0_125_22125">

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,9 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="#EF4136" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">
<rect x="2.2" y="2.2" width="19.6" height="19.6" rx="0.6"/>
<path d="M12 2.2V4.7a1.75 1.75 0 1 0 0 3.5V12"/>
<path d="M12 12v2.5a1.75 1.75 0 1 1 0 3.5v3.8"/>
<path d="M2.2 12h2.5a1.75 1.75 0 1 0 3.5 0H12"/>
<path d="M12 12h2.8a1.75 1.75 0 1 1 3.5 0h3.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#00BFB3" d="M13.394 0C8.683 0 4.609 2.716 2.644 6.667h15.641a4.77 4.77 0 0 0 3.073-1.11c.446-.375.864-.785 1.247-1.243l.001-.002A11.974 11.974 0 0 0 13.394 0zM1.804 8.889a12.009 12.009 0 0 0 0 6.222h14.7a3.111 3.111 0 1 0 0-6.222zm.84 8.444C4.61 21.283 8.684 24 13.395 24c3.701 0 7.011-1.677 9.212-4.312l-.001-.002a9.958 9.958 0 0 0-1.247-1.243 4.77 4.77 0 0 0-3.073-1.11z"/>
</svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@@ -1 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>
<svg fill="#F55036" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>

Before

Width:  |  Height:  |  Size: 568 B

After

Width:  |  Height:  |  Size: 563 B

View File

@@ -0,0 +1,74 @@
<svg role="img" viewBox="0 0 102.04 102.04" xmlns="http://www.w3.org/2000/svg">
<rect x="45.34" y="23.92" width=".41" height="15.95" transform="translate(-9.7 38.24) rotate(-41.55)" fill="#106DA9"/>
<rect x="32.04" y="45.06" width="11.92" height="11.92" transform="translate(-13.28 88.67) rotate(-89.6)" fill="#106DA9"/>
<rect x="45.04" y="32.06" width="11.92" height="11.92" transform="translate(12.63 88.76) rotate(-89.6)" fill="#106DA9"/>
<rect x="45.04" y="58.06" width="11.92" height="11.92" transform="translate(-13.37 114.58) rotate(-89.6)" fill="#106DA9"/>
<rect x="58.04" y="45.06" width="11.92" height="11.92" transform="translate(12.54 114.67) rotate(-89.6)" fill="#106DA9"/>
<rect x="57.03" y="22.05" width="7.94" height="7.94" transform="translate(34.56 86.84) rotate(-89.6)" fill="#106DA9"/>
<rect x="36.03" y="22.05" width="7.94" height="7.94" transform="translate(13.7 65.84) rotate(-89.6)" fill="#106DA9"/>
<rect x="22.03" y="37.05" width="7.94" height="7.94" transform="translate(-15.2 66.73) rotate(-89.6)" fill="#106DA9"/>
<rect x="22.03" y="58.05" width="7.94" height="7.94" transform="translate(-36.2 87.59) rotate(-89.6)" fill="#106DA9"/>
<rect x="72.03" y="58.05" width="7.94" height="7.94" transform="translate(13.45 137.58) rotate(-89.6)" fill="#106DA9"/>
<rect x="72.03" y="37.05" width="7.94" height="7.94" transform="translate(34.45 116.73) rotate(-89.6)" fill="#106DA9"/>
<rect x="15.02" y="26.04" width="5.96" height="5.96" transform="translate(-11.15 46.82) rotate(-89.6)" fill="#106DA9"/>
<rect x="25.02" y="14.04" width="5.96" height="5.96" transform="translate(10.79 44.9) rotate(-89.61)" fill="#106DA9"/>
<rect x="40.02" y="9.04" width="5.96" height="5.96" transform="translate(30.68 54.94) rotate(-89.6)" fill="#106DA9"/>
<rect x="11.02" y="41.04" width="5.96" height="5.96" transform="translate(-30.12 57.71) rotate(-89.6)" fill="#106DA9"/>
<rect x="81.98" y="26" width="5.04" height="5.04" transform="translate(-.2 .59) rotate(-.4)" fill="#106DA9"/>
<rect x="70.98" y="15" width="5.04" height="5.04" transform="translate(-.12 .51) rotate(-.4)" fill="#106DA9"/>
<rect x="55.98" y="9" width="5.04" height="5.04" transform="translate(-.08 .41) rotate(-.4)" fill="#106DA9"/>
<rect x="84.98" y="41" width="5.04" height="5.03" transform="translate(-.3 .6) rotate(-.4)" fill="#106DA9"/>
<rect x="36.03" y="72.05" width="7.94" height="7.94" transform="translate(-36.3 115.49) rotate(-89.6)" fill="#106DA9"/>
<rect x="57.03" y="72.05" width="7.94" height="7.94" transform="translate(-15.44 136.49) rotate(-89.6)" fill="#106DA9"/>
<rect x="81.02" y="70.04" width="5.96" height="5.96" transform="translate(10.39 156.51) rotate(-89.6)" fill="#106DA9"/>
<rect x="70.02" y="82.04" width="5.96" height="5.96" transform="translate(-12.52 157.43) rotate(-89.6)" fill="#106DA9"/>
<rect x="56.02" y="87.04" width="5.96" height="5.96" transform="translate(-31.44 148.37) rotate(-89.59)" fill="#106DA9"/>
<rect x="84.02" y="55.04" width="5.96" height="5.96" transform="translate(28.35 144.6) rotate(-89.59)" fill="#106DA9"/>
<rect x="14.98" y="71" width="5.04" height="5.04" transform="translate(-.51 .12) rotate(-.4)" fill="#106DA9"/>
<rect x="25.98" y="82" width="5.03" height="5.04" transform="translate(-.61 .21) rotate(-.41)" fill="#106DA9"/>
<rect x="40.98" y="87" width="5.04" height="5.04" transform="translate(-.62 .3) rotate(-.4)" fill="#106DA9"/>
<rect x="11.98" y="55" width="5.04" height="5.04" transform="translate(-.4 .1) rotate(-.4)" fill="#106DA9"/>
<rect x="18.01" y="12.03" width="2.98" height="2.98" transform="translate(5.85 32.93) rotate(-89.61)" fill="#106DA9"/>
<rect x=".01" y="45.03" width="2.98" height="2.98" transform="translate(-45.03 47.71) rotate(-89.61)" fill="#106DA9"/>
<rect x="2.01" y="35.03" width="2.98" height="2.98" transform="translate(-33.04 39.77) rotate(-89.61)" fill="#106DA9"/>
<rect x="35.01" y="2.03" width="2.98" height="2.98" transform="translate(32.73 40) rotate(-89.61)" fill="#106DA9"/>
<rect x="6.01" y="26.03" width="2.98" height="2.98" transform="translate(-20.07 34.83) rotate(-89.61)" fill="#106DA9"/>
<rect x="11.01" y="18.03" width="2.98" height="2.98" transform="translate(-7.11 31.89) rotate(-89.61)" fill="#106DA9"/>
<rect x="25.99" y="6.01" width="3.02" height="3.02" transform="translate(-.05 .19) rotate(-.39)" fill="#106DA9"/>
<rect x="44.01" y=".03" width="2.98" height="2.98" transform="translate(0 .31) rotate(-.39)" fill="#106DA9"/>
<rect x="93.01" y="26.03" width="2.98" height="2.98" transform="translate(66.34 121.83) rotate(-89.61)" fill="#106DA9"/>
<rect x="97.01" y="35.03" width="2.98" height="2.98" transform="translate(61.31 134.77) rotate(-89.61)" fill="#106DA9"/>
<rect x="88.01" y="18.03" width="2.98" height="2.98" transform="translate(69.37 108.89) rotate(-89.61)" fill="#106DA9"/>
<rect x="98.01" y="45.03" width="2.98" height="2.98" transform="translate(52.3 145.7) rotate(-89.61)" fill="#106DA9"/>
<rect x="53.99" y=".01" width="3.02" height="3.02" transform="translate(53.6 57.01) rotate(-89.61)" fill="#106DA9"/>
<rect x="64.01" y="2.03" width="2.98" height="2.98" transform="translate(61.53 69) rotate(-89.61)" fill="#106DA9"/>
<rect x="18.01" y="12.03" width="2.98" height="2.98" transform="translate(5.85 32.93) rotate(-89.61)" fill="#106DA9"/>
<rect x=".01" y="45.03" width="2.98" height="2.98" transform="translate(-45.03 47.71) rotate(-89.61)" fill="#106DA9"/>
<rect x="81.01" y="12.03" width="2.98" height="2.98" transform="translate(68.42 95.93) rotate(-89.61)" fill="#106DA9"/>
<rect x="2.01" y="35.03" width="2.98" height="2.98" transform="translate(-33.04 39.77) rotate(-89.61)" fill="#106DA9"/>
<rect x="35.01" y="2.03" width="2.98" height="2.98" transform="translate(32.73 40) rotate(-89.61)" fill="#106DA9"/>
<rect x="6.01" y="26.03" width="2.98" height="2.98" transform="translate(-20.07 34.83) rotate(-89.61)" fill="#106DA9"/>
<rect x="11.01" y="18.03" width="2.98" height="2.98" transform="translate(-7.11 31.89) rotate(-89.61)" fill="#106DA9"/>
<rect x="25.99" y="6.01" width="3.02" height="3.02" transform="translate(-.05 .19) rotate(-.39)" fill="#106DA9"/>
<rect x="44.01" y=".03" width="2.98" height="2.98" transform="translate(0 .31) rotate(-.39)" fill="#106DA9"/>
<rect x="73.01" y="6.03" width="2.98" height="2.98" transform="translate(66.47 81.97) rotate(-89.61)" fill="#106DA9"/>
<rect x="98.01" y="45.03" width="2.98" height="2.98" transform="translate(52.3 145.7) rotate(-89.61)" fill="#106DA9"/>
<rect x="53.99" y=".01" width="3.02" height="3.02" transform="translate(53.6 57.01) rotate(-89.61)" fill="#106DA9"/>
<rect x="64.01" y="2.03" width="2.98" height="2.98" transform="translate(61.53 69) rotate(-89.61)" fill="#106DA9"/>
<rect x="81.01" y="88.03" width="2.98" height="2.98" transform="translate(-7.58 171.41) rotate(-89.61)" fill="#106DA9"/>
<rect x="98.01" y="54.03" width="2.98" height="2.98" transform="translate(43.31 154.64) rotate(-89.61)" fill="#106DA9"/>
<rect x="18.01" y="88.03" width="2.98" height="2.98" transform="translate(-70.15 108.42) rotate(-89.61)" fill="#106DA9"/>
<rect x="97.01" y="64.03" width="2.98" height="2.98" transform="translate(32.32 163.58) rotate(-89.61)" fill="#106DA9"/>
<rect x="64.01" y="97.04" width="2.97" height="2.97" transform="translate(-33.67 163.03) rotate(-89.43)" fill="#106DA9"/>
<rect x="93.01" y="73.03" width="2.98" height="2.98" transform="translate(19.31 168.49) rotate(-89.59)" fill="#106DA9"/>
<rect x="88.01" y="81.03" width="2.98" height="2.98" transform="translate(6.37 171.45) rotate(-89.61)" fill="#106DA9"/>
<rect x="72.99" y="93.01" width="3.02" height="3.02" transform="translate(-.65 .51) rotate(-.39)" fill="#106DA9"/>
<rect x="53.99" y="99.01" width="3.02" height="3.02" transform="translate(-.68 .38) rotate(-.39)" fill="#106DA9"/>
<rect x="26.01" y="93.03" width="2.98" height="2.98" transform="translate(-67.21 121.37) rotate(-89.61)" fill="#106DA9"/>
<rect x="6.01" y="73.03" width="2.98" height="2.98" transform="translate(-67.07 81.51) rotate(-89.61)" fill="#106DA9"/>
<rect x="2.01" y="64.03" width="2.98" height="2.98" transform="translate(-62.04 68.57) rotate(-89.61)" fill="#106DA9"/>
<rect x="11.01" y="81.03" width="2.98" height="2.98" transform="translate(-70.1 94.46) rotate(-89.61)" fill="#106DA9"/>
<rect x=".01" y="54.03" width="2.98" height="2.98" transform="translate(-54.03 56.65) rotate(-89.61)" fill="#106DA9"/>
<rect x="45.01" y="99.03" width="2.98" height="2.98" transform="translate(-54.33 146.34) rotate(-89.61)" fill="#106DA9"/>
<rect x="35.01" y="97.03" width="2.98" height="2.98" transform="translate(-62.27 134.34) rotate(-89.61)" fill="#106DA9"/>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FFEC6E" d="m0 0 11.955 24L24 0zm13.366 4.827h1.393v1.38h-1.393zm-2.77 5.569H9.22V8.993h1.389zm0-2.087H9.22V6.906h1.389zm0-2.086H9.22V4.819h1.389zm2.087 6.263h-1.377V11.08h1.388zm0-2.09h-1.377V8.993h1.388zm0-2.087h-1.377V6.906h1.388zm0-2.086h-1.377V4.819h1.388zm.683.683h1.393v1.389h-1.393zm0 3.475V8.993h1.389v1.388Z"/></svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#9CA3AF" d="M9.755 1.52h-.001c-.31 0-.608.124-.828.343L4.037 6.752a1.17 1.17 0 0 1-.827.343H1.17A1.17 1.17 0 0 0 0 8.295l.052 1.984a1.17 1.17 0 0 0 1.17 1.14h2.37c.31 0 .607-.124.827-.344l4.93-4.93c.22-.22.517-.343.827-.343h2.874a1.17 1.17 0 0 0 1.17-1.17V2.69a1.17 1.17 0 0 0-1.17-1.17zm9.78 2.503c-.31 0-.608.123-.828.343l-4.889 4.889a1.17 1.17 0 0 1-.827.342h-2.756c-.31 0-.608.124-.827.344L4.15 15.197a1.17 1.17 0 0 1-.827.343H1.32a1.17 1.17 0 0 0-1.17 1.17v1.996c0 .646.524 1.17 1.17 1.17h2.017c.302 0 .592-.116.81-.325l5.535-5.304a1.17 1.17 0 0 1 .81-.326h2.88c.31 0 .607-.123.827-.342l4.93-4.93c.22-.22.517-.344.827-.344h2.873A1.17 1.17 0 0 0 24 7.135V5.193a1.17 1.17 0 0 0-1.17-1.17h-3.294zm0 8.559c-.31 0-.608.123-.828.343l-4.889 4.889a1.17 1.17 0 0 1-.827.343h-2.04a1.17 1.17 0 0 0-1.17 1.2l.052 1.984a1.17 1.17 0 0 0 1.17 1.14h2.37c.31 0 .607-.124.827-.343l4.93-4.93c.22-.22.517-.343.827-.343h2.873a1.17 1.17 0 0 0 1.17-1.17v-1.943a1.17 1.17 0 0 0-1.17-1.17h-3.294Z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,5 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2.34" y="2.34" width="19.27" height="19.27" rx="4.69" fill="#fff"/>
<circle cx="10.41" cy="11.95" r="2.77" fill="none" stroke="#000" stroke-width="1.36"/>
<rect x="15" y="8.53" width="1.36" height="6.94" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><title>PlanetScale</title><path d="M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a12 12 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.96 11.96 0 0 1 0 12m12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#9CA3AF" viewBox="0 0 24 24"><title>PlanetScale</title><path d="M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a12 12 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.96 11.96 0 0 1 0 12m12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24"/></svg>

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 321 B

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#FF6600" d="M23.035 9.601h-7.677a.956.956 0 01-.962-.962V.962a.956.956 0 00-.962-.956H10.56a.956.956 0 00-.962.956V8.64a.956.956 0 01-.962.962H5.762a.956.956 0 01-.961-.962V.962A.956.956 0 003.839 0H.959a.956.956 0 00-.956.962v22.076A.956.956 0 00.965 24h22.07a.956.956 0 00.962-.962V10.58a.956.956 0 00-.962-.98zm-3.86 8.152a1.437 1.437 0 01-1.437 1.443h-1.924a1.437 1.437 0 01-1.436-1.443v-1.917a1.437 1.437 0 011.436-1.443h1.924a1.437 1.437 0 011.437 1.443z"/>
</svg>

After

Width:  |  Height:  |  Size: 557 B

View File

@@ -1,3 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M.113 10.27A13.026 13.026 0 000 11.48h18.23c-.064-.125-.15-.237-.235-.347-3.117-4.027-4.793-3.677-7.19-3.78-.8-.034-1.34-.048-4.524-.048-1.704 0-3.555.005-5.358.01-.234.63-.459 1.24-.567 1.737h9.342v1.216H.113v.002zm18.26 2.426H.009c.02.326.05.645.094.961h16.955c.754 0 1.179-.429 1.315-.96zm-17.318 4.28s2.81 6.902 10.93 7.024c4.855 0 9.027-2.883 10.92-7.024H1.056zM11.988 0C7.5 0 3.593 2.466 1.531 6.108l4.75-.005v-.002c3.71 0 3.849.016 4.573.047l.448.016c1.563.052 3.485.22 4.996 1.364.82.621 2.007 1.99 2.712 2.965.654.902.842 1.94.396 2.934-.408.914-1.289 1.458-2.353 1.458H.391s.099.42.249.886h22.748A12.026 12.026 0 0024 12.005C24 5.377 18.621 0 11.988 0z"/>
<path fill="#9CA3AF" d="M.113 10.27A13.026 13.026 0 000 11.48h18.23c-.064-.125-.15-.237-.235-.347-3.117-4.027-4.793-3.677-7.19-3.78-.8-.034-1.34-.048-4.524-.048-1.704 0-3.555.005-5.358.01-.234.63-.459 1.24-.567 1.737h9.342v1.216H.113v.002zm18.26 2.426H.009c.02.326.05.645.094.961h16.955c.754 0 1.179-.429 1.315-.96zm-17.318 4.28s2.81 6.902 10.93 7.024c4.855 0 9.027-2.883 10.92-7.024H1.056zM11.988 0C7.5 0 3.593 2.466 1.531 6.108l4.75-.005v-.002c3.71 0 3.849.016 4.573.047l.448.016c1.563.052 3.485.22 4.996 1.364.82.621 2.007 1.99 2.712 2.965.654.902.842 1.94.396 2.934-.408.914-1.289 1.458-2.353 1.458H.391s.099.42.249.886h22.748A12.026 12.026 0 0024 12.005C24 5.377 18.621 0 11.988 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 771 B

View File

@@ -1,13 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 590 270">
<path d="M30.36,109.14v.48h0A3.73,3.73,0,0,1,30.36,109.14Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M30.36,109.14v.48h0A3.73,3.73,0,0,1,30.36,109.14Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M138.66,28.78C107.2,37.87,57.29,43,30.4,43h0V94.35a.8.8,0,0,0,.19.48c18.35,0,75-6,109.18-15.4a129,129,0,0,0,17.49-5.81c4.18-1.88,6.88-3.86,6.88-5.92V15.91C164.1,20.79,151.39,25.11,138.66,28.78Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M138.66,95.37c-18.83,5.43-44.24,9.47-67.39,11.83-15.54,1.59-30.06,2.42-40.87,2.42h0v51.31a.8.8,0,0,0,.19.48c18.35,0,75-6,109.18-15.39a130.38,130.38,0,0,0,17.49-5.81c4.18-1.89,6.88-3.86,6.88-5.92V82.5C164.1,87.37,151.39,91.69,138.66,95.37Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M138.66,162c-18.83,5.43-44.24,9.46-67.39,11.83-15.56,1.59-30.1,2.42-40.91,2.42V228c18.16,0,75.1-5.95,109.37-15.39,12.63-3.48,24.37-7.44,24.37-11.74V149.08C164.1,154,151.39,158.28,138.66,162Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M30.55,94.83C32.4,97.38,48,102.19,71.27,107.2c23.27,4.46,47.47,22.07,66.29,16.64,12.73-3.68,26.54-36.47,26.54-41.34V82c0-3.4-2.55-6.13-6.88-8.4-17.75-9.07-21.11-12.41-27.69-10.6C95.37,72.43,35.06,67.61,30.55,94.83Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M30.55,161.41C32.4,164,48,168.77,71.27,173.79c26,4.74,48.61,20.19,67.44,14.75,12.73-3.68,25.39-34.58,25.39-39.46v-.48c0-3.39-2.55-6.13-6.88-8.39-13.54-7.2-31.43-15.13-38-13.32C85,136.3,39.26,138.37,30.55,161.41Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M200.7,142.39c6,11.79,15.6,17.6,29.05,17.6,14.44,0,19.59-7.64,19.59-15.11,0-5.15-1.83-8.63-6.64-11.79-4.82-3.32-8.3-4.81-16.93-8-10.63-4-16.77-7-23.41-12.29-6.64-5.48-9.79-13-9.79-22.74a28.28,28.28,0,0,1,10.29-22.58c7-5.81,15.44-8.63,25.56-8.63,15.77,0,27.72,6.31,35.69,18.76L249.34,87.78c-4.48-6.81-11.29-10.3-20.59-10.3-9.13,0-15.77,5.15-15.77,12.29,0,4.81,2,7.14,4.82,10,1.82,1.33,6.47,3.32,8.63,4.48l6,2.32,6.8,2.66c11,4.48,18.76,9.3,23.57,14.44s7.31,12.12,7.31,20.75c0,20.42-14.11,34.2-40.51,34.2-21.41,0-37.18-10-44.48-26.4Z" fill="currentColor"/>
<path d="M354.25,104.71,342,117.49a28.14,28.14,0,0,0-21.24-9.13,25,25,0,0,0-18.43,7.47,27.76,27.76,0,0,0,0,37.52,25,25,0,0,0,18.43,7.47A28.14,28.14,0,0,0,342,151.69l12.29,12.78c-9,9.63-20.09,14.44-33.53,14.44-12.79,0-23.58-4.15-32.37-12.62s-13.12-19.09-13.12-31.7,4.32-23.08,13.12-31.54,19.58-12.78,32.37-12.78C334.16,90.27,345.28,95.08,354.25,104.71Z" fill="currentColor"/>
<path d="M393.88,125.62C408,124.3,413,122.47,413,116c0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a14.06,14.06,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C357.7,136.41,369.15,127.78,393.88,125.62ZM391.56,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.75,9.75,0,0,1-4.14,2.49c-3.82,1.33-6.31,1.66-14.28,2.49-11.62,1.33-17.43,5-17.43,10.79C377.12,158.33,382.43,162,391.56,162Z" fill="currentColor"/>
<path d="M444.84,60.88h19.92V149.2c0,8.13,2.66,11.62,10,11.62a21.15,21.15,0,0,0,6-.67v17.76a35.56,35.56,0,0,1-9.47,1c-17.59,0-26.39-9-26.39-27.06Z" fill="currentColor"/>
<path d="M521.71,125.62c14.11-1.32,19.09-3.15,19.09-9.62,0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a13.94,13.94,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C485.53,136.41,497,127.78,521.71,125.62ZM519.39,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.73,9.73,0,0,1-4.15,2.49c-3.81,1.33-6.3,1.66-14.27,2.49-11.62,1.33-17.43,5-17.43,10.79C505,158.33,510.26,162,519.39,162Z" fill="currentColor"/>
<path d="M30.55,94.83C32.4,97.38,48,102.19,71.27,107.2c23.27,4.46,47.47,22.07,66.29,16.64,12.73-3.68,26.54-36.47,26.54-41.34V82c0-3.4-2.55-6.13-6.88-8.4-17.75-9.07-21.11-12.41-27.69-10.6C95.37,72.43,35.06,67.61,30.55,94.83Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M30.55,161.41C32.4,164,48,168.77,71.27,173.79c26,4.74,48.61,20.19,67.44,14.75,12.73-3.68,25.39-34.58,25.39-39.46v-.48c0-3.39-2.55-6.13-6.88-8.39-13.54-7.2-31.43-15.13-38-13.32C85,136.3,39.26,138.37,30.55,161.41Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M200.7,142.39c6,11.79,15.6,17.6,29.05,17.6,14.44,0,19.59-7.64,19.59-15.11,0-5.15-1.83-8.63-6.64-11.79-4.82-3.32-8.3-4.81-16.93-8-10.63-4-16.77-7-23.41-12.29-6.64-5.48-9.79-13-9.79-22.74a28.28,28.28,0,0,1,10.29-22.58c7-5.81,15.44-8.63,25.56-8.63,15.77,0,27.72,6.31,35.69,18.76L249.34,87.78c-4.48-6.81-11.29-10.3-20.59-10.3-9.13,0-15.77,5.15-15.77,12.29,0,4.81,2,7.14,4.82,10,1.82,1.33,6.47,3.32,8.63,4.48l6,2.32,6.8,2.66c11,4.48,18.76,9.3,23.57,14.44s7.31,12.12,7.31,20.75c0,20.42-14.11,34.2-40.51,34.2-21.41,0-37.18-10-44.48-26.4Z" fill="#de3423"/>
<path d="M354.25,104.71,342,117.49a28.14,28.14,0,0,0-21.24-9.13,25,25,0,0,0-18.43,7.47,27.76,27.76,0,0,0,0,37.52,25,25,0,0,0,18.43,7.47A28.14,28.14,0,0,0,342,151.69l12.29,12.78c-9,9.63-20.09,14.44-33.53,14.44-12.79,0-23.58-4.15-32.37-12.62s-13.12-19.09-13.12-31.7,4.32-23.08,13.12-31.54,19.58-12.78,32.37-12.78C334.16,90.27,345.28,95.08,354.25,104.71Z" fill="#de3423"/>
<path d="M393.88,125.62C408,124.3,413,122.47,413,116c0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a14.06,14.06,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C357.7,136.41,369.15,127.78,393.88,125.62ZM391.56,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.75,9.75,0,0,1-4.14,2.49c-3.82,1.33-6.31,1.66-14.28,2.49-11.62,1.33-17.43,5-17.43,10.79C377.12,158.33,382.43,162,391.56,162Z" fill="#de3423"/>
<path d="M444.84,60.88h19.92V149.2c0,8.13,2.66,11.62,10,11.62a21.15,21.15,0,0,0,6-.67v17.76a35.56,35.56,0,0,1-9.47,1c-17.59,0-26.39-9-26.39-27.06Z" fill="#de3423"/>
<path d="M521.71,125.62c14.11-1.32,19.09-3.15,19.09-9.62,0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a13.94,13.94,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C485.53,136.41,497,127.78,521.71,125.62ZM519.39,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.73,9.73,0,0,1-4.15,2.49c-3.81,1.33-6.3,1.66-14.27,2.49-11.62,1.33-17.43,5-17.43,10.79C505,158.33,510.26,162,519.39,162Z" fill="#de3423"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -5,7 +5,7 @@ import { ArrowUpRight } from '@signozhq/icons';
const QUICK_FILTER_DOC_PATHS: Record<string, string> = {
severity_text: 'severity-text',
'deployment.environment': 'environment',
'deployment.environment.name': 'environment',
'service.name': 'service-name',
'host.name': 'hostname',
'k8s.cluster.name': 'k8s-cluster-name',

View File

@@ -0,0 +1,32 @@
import { Alert } from 'antd';
import { findOldSemconvNames } from 'utils/semconv';
interface SemconvEditorWarningProps {
value: unknown;
editor: string;
}
function SemconvEditorWarning({
value,
editor,
}: SemconvEditorWarningProps): JSX.Element | null {
const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
const renames = findOldSemconvNames(text);
if (renames.length === 0) {
return null;
}
return (
<Alert
type="warning"
showIcon
data-testid="semconv-editor-warning"
message={`${editor} contains renamed OpenTelemetry fields`}
description={renames
.map(({ old, current }) => `${old}${current}`)
.join(', ')}
/>
);
}
export default SemconvEditorWarning;

View File

@@ -0,0 +1,23 @@
import { Badge } from '@signozhq/ui/badge';
import { getSemconvRename } from 'utils/semconv';
interface SemconvOldNameBadgeProps {
name: string;
}
function SemconvOldNameBadge({
name,
}: SemconvOldNameBadgeProps): JSX.Element | null {
const rename = getSemconvRename(name);
if (!rename || rename.family.kind !== 'attribute') {
return null;
}
return (
<Badge color="amber" variant="outline" data-testid="semconv-old-name-badge">
old name, renamed to {rename.current}
</Badge>
);
}
export default SemconvOldNameBadge;

View File

@@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/react';
import SemconvEditorWarning from '../SemconvEditorWarning';
import SemconvOldNameBadge from '../SemconvOldNameBadge';
describe('semantic convention product hints', () => {
it('badges an old raw attribute with its current name', () => {
render(<SemconvOldNameBadge name="deployment.environment" />);
expect(screen.getByTestId('semconv-old-name-badge')).toHaveTextContent(
'old name, renamed to deployment.environment.name',
);
});
it('does not badge a current raw attribute', () => {
render(<SemconvOldNameBadge name="deployment.environment.name" />);
expect(
screen.queryByTestId('semconv-old-name-badge'),
).not.toBeInTheDocument();
});
it('shows an informational editor warning without disabling the editor', () => {
render(
<>
<input aria-label="query" defaultValue="db.system = 'postgresql'" />
<SemconvEditorWarning
value="db.system = 'postgresql'"
editor="ClickHouse SQL"
/>
</>,
);
expect(screen.getByLabelText('query')).not.toBeDisabled();
expect(screen.getByTestId('semconv-editor-warning')).toHaveTextContent(
'db.system → db.system.name',
);
});
});

View File

@@ -0,0 +1,2 @@
export { default as SemconvEditorWarning } from './SemconvEditorWarning';
export { default as SemconvOldNameBadge } from './SemconvOldNameBadge';

View File

@@ -0,0 +1,59 @@
// Code generated by scripts/semconv. DO NOT EDIT.
export type SemconvFamily = {
readonly current: string;
readonly old: readonly string[];
readonly kind: 'attribute' | 'metric';
readonly contexts: readonly string[];
readonly signals: readonly string[];
readonly applyToMetrics: readonly string[];
readonly valueMap: Readonly<Record<string, string>>;
};
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
{
current: 'container.cpu.usage',
old: ['container.cpu.utilization'],
kind: 'metric',
contexts: ['metric'],
signals: ['metrics'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'db.system.name',
old: ['db.system'],
kind: 'attribute',
contexts: ['attribute', 'resource'],
signals: ['logs', 'metrics', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'deployment.environment.name',
old: ['deployment.environment'],
kind: 'attribute',
contexts: ['attribute', 'resource'],
signals: ['logs', 'metrics', 'traces'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'k8s.node.cpu.usage',
old: ['k8s.node.cpu.utilization'],
kind: 'metric',
contexts: ['metric'],
signals: ['metrics'],
applyToMetrics: [],
valueMap: {},
},
{
current: 'k8s.pod.cpu.usage',
old: ['k8s.pod.cpu.utilization'],
kind: 'metric',
contexts: ['metric'],
signals: ['metrics'],
applyToMetrics: [],
valueMap: {},
},
] as const;

View File

@@ -155,7 +155,7 @@ function DomainList(): JSX.Element {
dataSource={DataSource.TRACES}
queryData={query}
onChange={handleSearchChange}
placeholder="Enter your filter query (e.g., deployment.environment = 'otel-demo' AND service.name = 'frontend')"
placeholder="Enter your filter query (e.g., deployment.environment.name = 'otel-demo' AND service.name = 'frontend')"
hardcodedAttributeKeys={ApiMonitoringHardcodedAttributeKeys}
/>
</div>

View File

@@ -6,9 +6,9 @@ import { SPAN_ATTRIBUTES } from './Explorer/Domains/DomainDetails/constants';
export const ApiMonitoringHardcodedAttributeKeys: QueryKeyDataSuggestionsProps[] =
[
{
label: 'deployment.environment',
label: 'deployment.environment.name',
type: 'resource',
name: 'deployment.environment',
name: 'deployment.environment.name',
signal: 'traces',
fieldDataType: QUERY_BUILDER_KEY_TYPES.STRING,
},

View File

@@ -87,7 +87,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
key: 'deployment.environment.name',
dataType: DataTypes.String,
type: 'resource',
},

View File

@@ -112,7 +112,7 @@ export const INFRA_MONITORING_ATTR_KEYS = {
K8S_OBJECT_NAME: 'k8s.object.name',
// Environment
DEPLOYMENT_ENVIRONMENT: 'deployment.environment',
DEPLOYMENT_ENVIRONMENT: 'deployment.environment.name',
// Host System
OS_TYPE: 'os.type',
@@ -733,7 +733,7 @@ export const ENTITY_FILTER_PLACEHOLDERS: Record<InfraMonitoringEntity, string> =
[InfraMonitoringEntity.NAMESPACES]:
"Enter your filter query (e.g., k8s.namespace.name = 'production' AND k8s.cluster.name = 'prod-cluster')",
[InfraMonitoringEntity.CLUSTERS]:
"Enter your filter query (e.g., k8s.cluster.name = 'prod-cluster' AND deployment.environment = 'production')",
"Enter your filter query (e.g., k8s.cluster.name = 'prod-cluster' AND deployment.environment.name = 'production')",
[InfraMonitoringEntity.DEPLOYMENTS]:
"Enter your filter query (e.g., k8s.deployment.name = 'api-server' AND k8s.namespace.name = 'production')",
[InfraMonitoringEntity.STATEFULSETS]:

View File

@@ -2,6 +2,17 @@
color: white;
}
.semconv-migration-report {
margin-top: 32px;
display: flex;
flex-direction: column;
gap: 12px;
.ant-table-wrapper {
margin-top: 4px;
}
}
.ingestion-key-container {
margin-top: 24px;
display: flex;

View File

@@ -5,6 +5,8 @@ import getIngestionData from 'api/settings/getIngestionData';
import { useAppContext } from 'providers/App/App';
import { IngestionDataType } from 'types/api/settings/ingestion';
import SemconvMigrationReport from './SemconvMigrationReport';
import './IngestionSettings.styles.scss';
export default function IngestionSettings(): JSX.Element {
@@ -84,6 +86,7 @@ export default function IngestionSettings(): JSX.Element {
dataSource={data}
bordered
/>
<SemconvMigrationReport />
</div>
);
}

View File

@@ -83,6 +83,8 @@ import { MeterAggregateOperator } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { getDaysUntilExpiry } from 'utils/timeUtils';
import SemconvMigrationReport from './SemconvMigrationReport';
import './IngestionSettings.styles.scss';
const { Option } = Select;
@@ -1705,6 +1707,7 @@ function MultiIngestionSettings(): JSX.Element {
}}
className="ingestion-keys-table"
/>
<SemconvMigrationReport />
</div>
{/* Delete Key Modal */}

View File

@@ -0,0 +1,72 @@
import { useQuery } from 'react-query';
import { Alert, Table, TableColumnsType } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import getSemconvMigrationReport from 'api/semconv/getMigrationReport';
import dayjs from 'dayjs';
import { SemconvMigrationReportEntry } from 'types/api/semconvMigration';
function SemconvMigrationReport(): JSX.Element {
const { data, isLoading, isError } = useQuery({
queryKey: ['semconv-migration-report'],
queryFn: getSemconvMigrationReport,
});
const columns: TableColumnsType<SemconvMigrationReportEntry> = [
{
title: 'Old name',
dataIndex: 'old',
key: 'old',
},
{
title: 'Current name',
dataIndex: 'current',
key: 'current',
},
{
title: 'Signal',
dataIndex: 'signal',
key: 'signal',
},
{
title: 'Services still sending only the old name',
dataIndex: 'services',
key: 'services',
render: (services: string[]): string => services.join(', '),
},
{
title: 'Last seen',
dataIndex: 'lastSeenUnixMilli',
key: 'lastSeenUnixMilli',
render: (value: number): string =>
dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
];
return (
<section className="semconv-migration-report">
<Typography.Title level={4}>Semantic convention migration</Typography.Title>
<Typography.Text>
Services in this report sent an old OpenTelemetry field during the last 24
hours without sending its current replacement. Update their SDK or
instrumentation when practical; SigNoz queries remain backward compatible.
</Typography.Text>
{isError && (
<Alert
type="error"
showIcon
message="Could not load the semantic convention migration report"
/>
)}
<Table
loading={isLoading}
columns={columns}
dataSource={data?.entries ?? []}
rowKey={(entry): string => `${entry.current}-${entry.old}-${entry.signal}`}
pagination={false}
locale={{ emptyText: 'No old-only services found in the last 24 hours' }}
/>
</section>
);
}
export default SemconvMigrationReport;

View File

@@ -15,7 +15,7 @@ export const SAMPLE_SPAN_JSON = `{
},
"resource": {
"service.name": "llm-gateway",
"deployment.environment": "production"
"deployment.environment.name": "production"
}
}`;

View File

@@ -1120,7 +1120,7 @@
"plugin": {
"kind": "signoz/QueryVariable",
"spec": {
"queryValue": "SELECT DISTINCT resources_string['deployment.environment'] AS environment FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'deployment.environment') AND timestamp >= now() - INTERVAL 1 DAY"
"queryValue": "SELECT DISTINCT resources_string['deployment.environment.name'] AS environment FROM signoz_traces.distributed_signoz_index_v3 WHERE mapContains(resources_string, 'deployment.environment.name') AND timestamp >= now() - INTERVAL 1 DAY"
}
}
}

View File

@@ -1,6 +1,7 @@
import { Divider } from '@signozhq/ui/divider';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { SemconvOldNameBadge } from 'components/Semconv';
import { TagContainer, TagLabel, TagValue } from './FieldRenderer.styles';
import { FieldRendererProps } from './LogDetailedView.types';
@@ -28,6 +29,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<Typography.Text truncate={1} className="label">
{newField}{' '}
</Typography.Text>
<SemconvOldNameBadge name={newField} />
</TooltipSimple>
<div className="tags">
@@ -47,7 +49,10 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
</div>
</>
) : (
<span className="label">{field}</span>
<>
<span className="label">{field}</span>
<SemconvOldNameBadge name={field} />
</>
)}
</span>
);

View File

@@ -164,7 +164,9 @@ describe('useInitialQuery - Priority-Based Resource Filtering', () => {
value: 'frontend-service',
}),
expect.objectContaining({
key: expect.objectContaining({ key: 'deployment.environment' }),
key: expect.objectContaining({
key: 'deployment.environment.name',
}),
value: 'production',
}),
expect.objectContaining({
@@ -286,7 +288,9 @@ describe('useInitialQuery - Priority-Based Resource Filtering', () => {
value: 'legacy-app',
}),
expect.objectContaining({
key: expect.objectContaining({ key: 'deployment.environment' }),
key: expect.objectContaining({
key: 'deployment.environment.name',
}),
value: 'production',
}),
expect.objectContaining({

View File

@@ -6,13 +6,14 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { getSemconvRename } from 'utils/semconv';
const FALLBACK_STARTS_WITH_REGEX = /^(k8s|cloud|host|deployment)/; // regex to filter out resources that start with the specified keywords
const FALLBACK_CONTAINS_REGEX = /(env|service|file|container|tenant)/; // regex to filter out resources that contains the specified keywords
// Priority categories for filter selection
// Strategy:
// - Always include: service.name, deployment.environment, env, environment
// - Always include: service.name, deployment.environment.name, env, environment
// - Select ONE category only: stops at the first category with a matching attribute
// - Within category: picks the first available attribute by order
// - Order (highest to lowest priority): Kubernetes > Cloud > Host > Container
@@ -26,27 +27,36 @@ const PRIORITY_CATEGORIES = [
const SERVICE_AND_ENVIRONMENT_KEYS = [
'service.name',
'deployment.environment',
'deployment.environment.name',
'env',
'environment',
];
export const getFiltersFromResources = (
resources: ILog['resources_string'],
): TagFilterItem[] =>
Object.keys(resources).map((key: string) => {
): TagFilterItem[] => {
const items = new Map<string, TagFilterItem>();
Object.keys(resources).forEach((key: string) => {
const currentKey = getSemconvRename(key)?.current ?? key;
const resourceValue = resources[key] as string;
return {
const item = {
id: uuid(),
key: {
key,
key: currentKey,
dataType: DataTypes.String,
type: 'resource',
},
op: OPERATORS['='],
value: resourceValue,
};
// If raw data contains both names, retain the current value just like the
// backend's current-first resolver.
if (!items.has(currentKey) || key === currentKey) {
items.set(currentKey, item);
}
});
return Array.from(items.values());
};
export const isServiceOrEnvironmentAttribute = (key: string): boolean =>
SERVICE_AND_ENVIRONMENT_KEYS.includes(key);

View File

@@ -94,7 +94,7 @@ function DBCall(): JSX.Element {
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const legend = dotMetricsEnabled ? '{{db.system}}' : '{{db_system}}';
const legend = dotMetricsEnabled ? '{{db.system.name}}' : '{{db_system_name}}';
const databaseCallsRPSWidget = useMemo(
() =>

View File

@@ -28,7 +28,7 @@ import { v4 as uuid } from 'uuid';
export const dbSystemTags: Tags[] = [
{
Key: 'db.system.(string)',
Key: 'db.system.name.(string)',
StringValues: [''],
NumberValues: [],
BoolValues: [],

View File

@@ -103,7 +103,7 @@ export enum WidgetKeys {
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
Signoz_latency_bucket_norm = 'signoz_latency_bucket',
Signoz_latency_bucket = 'signoz_latency.bucket',
Db_system = 'db.system',
Db_system = 'db.system.name',
Db_system_norm = 'db_system',
}

View File

@@ -2,6 +2,7 @@ import { ChangeEvent, useCallback } from 'react';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd';
import { SemconvEditorWarning } from 'components/Semconv';
import { LEGEND } from 'constants/global';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -118,6 +119,7 @@ function ClickHouseQueryBuilder({
theme={isDarkMode ? 'my-theme' : 'light'}
beforeMount={setEditorTheme}
/>
<SemconvEditorWarning value={queryData?.query} editor="ClickHouse SQL" />
<Input
onChange={handleUpdateInput}
name="legend"

View File

@@ -1,6 +1,7 @@
import { ChangeEvent, useCallback } from 'react';
import { Input } from 'antd';
import { LEGEND } from 'constants/global';
import { SemconvEditorWarning } from 'components/Semconv';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IPromQLQuery } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -66,6 +67,7 @@ function PromQLQueryBuilder({
style={{ marginBottom: '0.5rem' }}
data-testid="promql-query-input"
/>
<SemconvEditorWarning value={queryData?.query} editor="PromQL" />
<Input
onChange={handleUpdateQuery}

View File

@@ -5,9 +5,11 @@ import androidJavaMonitoringUrl from '@/assets/Logos/android-java-monitoring.svg
import androidKotlinMonitoringUrl from '@/assets/Logos/android-kotlin-monitoring.svg';
import anthropicApiMonitoringUrl from '@/assets/Logos/anthropic-api-monitoring.svg';
import apacheDruidUrl from '@/assets/Logos/apache-druid.svg';
import apacheUrl from '@/assets/Logos/apache.svg';
import apiGatewayUrl from '@/assets/Logos/api-gateway.svg';
import argocdUrl from '@/assets/Logos/argocd.svg';
import aspnetUrl from '@/assets/Logos/aspnet.svg';
import auth0Url from '@/assets/Logos/auth0.svg';
import autogenUrl from '@/assets/Logos/autogen.svg';
import awsAlbUrl from '@/assets/Logos/aws-alb.svg';
import azureAppServiceUrl from '@/assets/Logos/azure-app-service.svg';
@@ -27,6 +29,7 @@ import claudeCodeUrl from '@/assets/Logos/claude-code.svg';
import clickhouseUrl from '@/assets/Logos/clickhouse.svg';
import cloudflareUrl from '@/assets/Logos/cloudflare.svg';
import cloudwatchLogsUrl from '@/assets/Logos/cloudwatch-logs.svg';
import cohereUrl from '@/assets/Logos/cohere.svg';
import confluentKafkaUrl from '@/assets/Logos/confluent-kafka.svg';
import convexLogoUrl from '@/assets/Logos/convex-logo.svg';
import cppUrl from '@/assets/Logos/cpp.svg';
@@ -39,11 +42,13 @@ import denoUrl from '@/assets/Logos/deno.svg';
import dockerUrl from '@/assets/Logos/docker.svg';
import documentLoadUrl from '@/assets/Logos/document-load.svg';
import dotnetUrl from '@/assets/Logos/dotnet.svg';
import dspyUrl from '@/assets/Logos/dspy.svg';
import dynamodbUrl from '@/assets/Logos/dynamodb.svg';
import ec2Url from '@/assets/Logos/ec2.svg';
import ecsUrl from '@/assets/Logos/ecs.svg';
import eksUrl from '@/assets/Logos/eks.svg';
import elasticacheUrl from '@/assets/Logos/elasticache.svg';
import elasticsearchUrl from '@/assets/Logos/elasticsearch.svg';
import elbUrl from '@/assets/Logos/elb.svg';
import elixirUrl from '@/assets/Logos/elixir.svg';
import elkUrl from '@/assets/Logos/elk.svg';
@@ -73,8 +78,10 @@ import grafanaUrl from '@/assets/Logos/grafana.svg';
import graphqlUrl from '@/assets/Logos/graphql.svg';
import grokUrl from '@/assets/Logos/grok.svg';
import groqUrl from '@/assets/Logos/groq.svg';
import haproxyUrl from '@/assets/Logos/haproxy.svg';
import hasuraUrl from '@/assets/Logos/hasura.svg';
import haystackUrl from '@/assets/Logos/haystack.svg';
import hcpVaultUrl from '@/assets/Logos/hcp-vault.svg';
import herokuUrl from '@/assets/Logos/heroku.svg';
import honeycombUrl from '@/assets/Logos/honeycomb.svg';
import hostmetricsUrl from '@/assets/Logos/hostmetrics.svg';
@@ -92,6 +99,7 @@ import kafkaUrl from '@/assets/Logos/kafka.svg';
import kubernetesUrl from '@/assets/Logos/kubernetes.svg';
import lambdaUrl from '@/assets/Logos/lambda.svg';
import langchainUrl from '@/assets/Logos/langchain.svg';
import langflowUrl from '@/assets/Logos/langflow.svg';
import langtraceUrl from '@/assets/Logos/langtrace.svg';
import litellmUrl from '@/assets/Logos/litellm.svg';
import livekitUrl from '@/assets/Logos/livekit.svg';
@@ -117,6 +125,7 @@ import ollamaUrl from '@/assets/Logos/ollama.svg';
import openaiUrl from '@/assets/Logos/openai.svg';
import openclawUrl from '@/assets/Logos/openclaw.svg';
import opencodeUrl from '@/assets/Logos/opencode.svg';
import openWebuiUrl from '@/assets/Logos/open-webui.svg';
import openlitUrl from '@/assets/Logos/openlit.svg';
import openrouterUrl from '@/assets/Logos/openrouter.svg';
import opentelemetryUrl from '@/assets/Logos/opentelemetry.svg';
@@ -131,6 +140,7 @@ import pythonUrl from '@/assets/Logos/python.svg';
import quarkusUrl from '@/assets/Logos/quarkus.svg';
import quickstartUrl from '@/assets/Logos/quickstart.svg';
import qwenUrl from '@/assets/Logos/qwen.svg';
import rabbitmqUrl from '@/assets/Logos/rabbitmq.svg';
import railwayUrl from '@/assets/Logos/railway.svg';
import rdsUrl from '@/assets/Logos/rds.svg';
import reactjsUrl from '@/assets/Logos/reactjs.svg';
@@ -3937,6 +3947,58 @@ const onboardingConfigWithLinks = [
],
link: '/docs/claude-code-monitoring/',
},
{
dataSource: 'cohere',
label: 'Cohere',
imgUrl: cohereUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'cohere',
'cohere api',
'cohere logs',
'cohere metrics',
'cohere monitoring',
'cohere observability',
'cohere traces',
'llm',
'llm monitoring',
'logging',
'logs',
'metrics',
'monitoring',
'observability',
'otel cohere integration',
'telemetry',
],
link: '/docs/cohere-monitoring/',
},
{
dataSource: 'langflow',
label: 'Langflow',
imgUrl: langflowUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'langflow',
'langflow logs',
'langflow metrics',
'langflow monitoring',
'langflow observability',
'langflow traces',
'llm',
'llm monitoring',
'logging',
'logs',
'low code ai',
'metrics',
'monitoring',
'observability',
'otel langflow integration',
'telemetry',
],
link: '/docs/langflow-observability/',
},
{
dataSource: 'deepseek-api',
label: 'DeepSeek API',
@@ -5483,12 +5545,32 @@ const onboardingConfigWithLinks = [
relatedSearchKeywords: [
'infrastructure',
'traefik',
'traefik access logs',
'traefik logs',
'traefik metrics',
'traefik monitoring',
'traefik observability',
'traefik tracing',
],
link: '/docs/tutorial/traefik-observability/',
question: {
desc: 'Which Traefik signals do you want to send to SigNoz?',
type: 'select',
options: [
{
key: 'traefik-metrics-traces',
label: 'Metrics & Traces',
imgUrl: opentelemetryUrl,
link: '/docs/tutorial/traefik-observability/',
},
{
key: 'traefik-logs',
label: 'Access Logs',
imgUrl: opentelemetryUrl,
link: '/docs/integrations/opentelemetry-traefik/',
},
],
},
},
{
dataSource: 'mongodb-atlas',
@@ -5518,11 +5600,32 @@ const onboardingConfigWithLinks = [
relatedSearchKeywords: [
'database',
'mysql',
'mysql error log',
'mysql logs',
'mysql metrics',
'mysql monitoring',
'mysql observability',
'mysql slow query log',
],
link: '/docs/metrics-management/mysql-metrics/',
question: {
desc: 'Which MySQL signals do you want to send to SigNoz?',
type: 'select',
options: [
{
key: 'mysql-metrics',
label: 'Metrics',
imgUrl: opentelemetryUrl,
link: '/docs/metrics-management/mysql-metrics/',
},
{
key: 'mysql-logs',
label: 'Logs',
imgUrl: opentelemetryUrl,
link: '/docs/integrations/opentelemetry-mysql/',
},
],
},
},
{
dataSource: 'jmx',
@@ -6467,6 +6570,30 @@ const onboardingConfigWithLinks = [
id: 'cert-manager',
link: '/docs/infrastructure-monitoring/cert-manager/',
},
{
dataSource: 'pgbouncer',
label: 'PgBouncer',
imgUrl: postgresqlUrl,
tags: ['infrastructure monitoring', 'metrics'],
module: 'metrics',
relatedSearchKeywords: [
'connection pooler',
'connection pooling',
'database',
'metrics',
'monitoring',
'observability',
'opentelemetry pgbouncer',
'pgbouncer',
'pgbouncer metrics',
'pgbouncer monitoring',
'pgbouncer observability',
'postgres',
'postgresql',
],
id: 'pgbouncer',
link: '/docs/metrics-management/opentelemetry-pgbouncer/',
},
{
dataSource: 'graphql',
label: 'GraphQL',
@@ -6491,6 +6618,28 @@ const onboardingConfigWithLinks = [
id: 'graphql',
link: '/docs/instrumentation/javascript/opentelemetry-graphql/',
},
{
dataSource: 'opentelemetry-ebpf',
label: 'OpenTelemetry eBPF (OBI)',
imgUrl: opentelemetryUrl,
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'auto instrumentation',
'ebpf',
'obi',
'opentelemetry ebpf',
'opentelemetry obi',
'otel ebpf',
'zero code instrumentation',
'monitoring',
'observability',
'traces',
'tracing',
],
id: 'opentelemetry-ebpf',
link: '/docs/instrumentation/opentelemetry-ebpf/',
},
{
dataSource: 'railway',
label: 'Railway',
@@ -6513,6 +6662,54 @@ const onboardingConfigWithLinks = [
id: 'railway',
link: '/docs/integrations/outposts/railway/',
},
{
dataSource: 'hcp-vault',
label: 'HCP Vault',
imgUrl: hcpVaultUrl,
tags: ['logs'],
module: 'logs',
relatedSearchKeywords: [
'hashicorp',
'hashicorp vault',
'hcp',
'hcp vault',
'hcp vault logs',
'hcp vault monitoring',
'hcp vault observability',
'log forwarding',
'logging',
'logs',
'monitoring',
'observability',
'secrets management',
'vault',
],
id: 'hcp-vault',
link: '/docs/integrations/outposts/hcp-vault/',
},
{
dataSource: 'auth0',
label: 'Auth0',
imgUrl: auth0Url,
tags: ['logs'],
module: 'logs',
relatedSearchKeywords: [
'auth0',
'auth0 logs',
'auth0 monitoring',
'auth0 observability',
'authentication',
'authorization',
'identity',
'log forwarding',
'logging',
'logs',
'monitoring',
'observability',
],
id: 'auth0',
link: '/docs/integrations/outposts/auth0/',
},
{
dataSource: 'aspnet-core-metrics',
label: 'ASP.NET Core Metrics',
@@ -6632,5 +6829,164 @@ const onboardingConfigWithLinks = [
id: 'apache-druid',
link: '/docs/integrations/opentelemetry-apache-druid/',
},
{
dataSource: 'apache',
label: 'Apache HTTP Server',
imgUrl: apacheUrl,
tags: ['infrastructure monitoring', 'metrics', 'logs'],
module: 'metrics',
relatedSearchKeywords: [
'apache',
'apache access logs',
'apache error logs',
'apache http server',
'apache httpd',
'apache logs',
'apache metrics',
'apache monitoring',
'apache observability',
'httpd',
'infrastructure monitoring',
'logs',
'metrics',
'mod_status',
'monitoring',
'observability',
'opentelemetry apache',
'web server',
],
id: 'apache',
link: '/docs/integrations/opentelemetry-apache/',
},
{
dataSource: 'haproxy',
label: 'HAProxy',
imgUrl: haproxyUrl,
tags: ['infrastructure monitoring', 'metrics', 'logs'],
module: 'metrics',
relatedSearchKeywords: [
'haproxy',
'haproxy logs',
'haproxy metrics',
'haproxy monitoring',
'haproxy observability',
'infrastructure monitoring',
'load balancer',
'logs',
'metrics',
'monitoring',
'observability',
'opentelemetry haproxy',
'proxy',
'reverse proxy',
'syslog',
],
id: 'haproxy',
link: '/docs/integrations/opentelemetry-haproxy/',
},
{
dataSource: 'elasticsearch',
label: 'Elasticsearch',
imgUrl: elasticsearchUrl,
tags: ['database'],
module: 'metrics',
relatedSearchKeywords: [
'cluster health',
'database',
'elastic',
'elasticsearch',
'elasticsearch logs',
'elasticsearch metrics',
'elasticsearch monitoring',
'elasticsearch observability',
'logs',
'metrics',
'monitoring',
'observability',
'opentelemetry elasticsearch',
'search engine',
],
id: 'elasticsearch',
link: '/docs/integrations/opentelemetry-elasticsearch/',
},
{
dataSource: 'rabbitmq',
label: 'RabbitMQ',
imgUrl: rabbitmqUrl,
tags: ['Messaging Queues'],
module: 'metrics',
relatedSearchKeywords: [
'amqp',
'broker',
'logs',
'messaging',
'messaging queues',
'metrics',
'monitoring',
'observability',
'opentelemetry rabbitmq',
'queues',
'rabbitmq',
'rabbitmq logs',
'rabbitmq metrics',
'rabbitmq monitoring',
'rabbitmq observability',
],
id: 'rabbitmq',
link: '/docs/integrations/opentelemetry-rabbitmq/',
},
{
dataSource: 'open-webui',
label: 'Open WebUI',
imgUrl: openWebuiUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'llm',
'llm monitoring',
'logs',
'metrics',
'monitoring',
'observability',
'open webui',
'open webui logs',
'open webui metrics',
'open webui monitoring',
'open webui observability',
'open webui traces',
'openlit',
'openwebui',
'otel open webui integration',
'self hosted chat ui',
'traces',
'tracing',
],
id: 'open-webui',
link: '/docs/open-webui-monitoring/',
},
{
dataSource: 'dspy',
label: 'DSPy',
imgUrl: dspyUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'dspy',
'dspy monitoring',
'dspy observability',
'dspy traces',
'llm',
'llm monitoring',
'monitoring',
'observability',
'openinference',
'otel dspy integration',
'prompt optimization',
'traces',
'tracing',
],
id: 'dspy',
link: '/docs/dspy-observability/',
},
];
export default onboardingConfigWithLinks;

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Form } from 'antd';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import { SemconvEditorWarning } from 'components/Semconv';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import isEqual from 'lodash-es/isEqual';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
@@ -55,6 +56,7 @@ function TagFilterInputWithLogsResultPreview({
value={value}
onChange={onChange}
/>
<SemconvEditorWarning value={value} editor="Pipeline filter" />
<div className="pipeline-filter-input-preview-container">
<LogsFilterPreview filter={value} />
</div>

View File

@@ -424,7 +424,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
expect(result.current.queries).toHaveLength(1);
expect(result.current.queries[0]).toMatchObject({
tagKey: 'resource_deployment_environment',
tagKey: 'resource_deployment_environment_name',
operator: 'IN',
tagValue: ['production'],
});
@@ -435,7 +435,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment_environment',
tagKey: 'resource_deployment_environment_name',
operator: 'IN',
tagValue: ['production'],
},
@@ -459,7 +459,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const tagKeys = result.current.queries.map((q) => q.tagKey);
expect(tagKeys).not.toContain('resource_deployment_environment');
expect(tagKeys).not.toContain('resource_deployment_environment_name');
expect(tagKeys).toContain('resource_service_name');
});
});
@@ -468,7 +468,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment_environment',
tagKey: 'resource_deployment_environment_name',
operator: 'IN',
tagValue: ['production'],
},
@@ -486,7 +486,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const envQueries = result.current.queries.filter(
(q) => q.tagKey === 'resource_deployment_environment',
(q) => q.tagKey === 'resource_deployment_environment_name',
);
expect(envQueries).toHaveLength(1);
expect(envQueries[0].tagValue).toStrictEqual(['staging']);
@@ -518,7 +518,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
expect(result.current.queries[0].tagKey).toBe(
'resource_deployment.environment',
'resource_deployment.environment.name',
);
});
});

View File

@@ -6,13 +6,13 @@ import { mappingWithRoutesAndKeys } from '../utils';
describe('useResourceAttribute config', () => {
describe('whilelistedKeys', () => {
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
expect(whilelistedKeys).toContain('resource_deployment_environment');
expect(whilelistedKeys).toContain('resource_deployment_environment_name');
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
});
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
expect(whilelistedKeys).toContain('resource_deployment.environment');
expect(whilelistedKeys).toContain('resource_deployment.environment.name');
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
});
@@ -21,8 +21,8 @@ describe('useResourceAttribute config', () => {
describe('mappingWithRoutesAndKeys', () => {
const dotNotationFilters = [
{
label: 'deployment.environment',
value: 'resource_deployment.environment',
label: 'deployment.environment.name',
value: 'resource_deployment.environment.name',
},
{ label: 'k8s.cluster.name', value: 'resource_k8s.cluster.name' },
{ label: 'k8s.cluster.namespace', value: 'resource_k8s.cluster.namespace' },
@@ -30,8 +30,8 @@ describe('useResourceAttribute config', () => {
const underscoreNotationFilters = [
{
label: 'deployment.environment',
value: 'resource_deployment_environment',
label: 'deployment.environment.name',
value: 'resource_deployment_environment_name',
},
{ label: 'k8s.cluster.name', value: 'resource_k8s_cluster_name' },
{ label: 'k8s.cluster.namespace', value: 'resource_k8s_cluster_namespace' },

View File

@@ -1,6 +1,6 @@
export const whilelistedKeys = [
'resource_deployment_environment',
'resource_deployment.environment',
'resource_deployment_environment_name',
'resource_deployment.environment.name',
'resource_k8s_cluster_name',
'resource_k8s.cluster.name',
'resource_k8s_cluster_namespace',

View File

@@ -148,9 +148,9 @@ export const getResourceDeploymentKeys = (
dotMetricsEnabled: boolean,
): string => {
if (dotMetricsEnabled) {
return 'resource_deployment.environment';
return 'resource_deployment.environment.name';
}
return 'resource_deployment_environment';
return 'resource_deployment_environment_name';
};
export const GetTagKeys = async (

View File

@@ -40,7 +40,7 @@ export const LogsQuickFiltersConfig: IQuickFiltersConfig[] = [
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
key: 'deployment.environment.name',
dataType: DataTypes.String,
type: 'resource',
},

View File

@@ -11,6 +11,7 @@ import { Skeleton } from 'antd';
import { DetailsHeader, DetailsPanelDrawer } from 'components/DetailsPanel';
import { HeaderAction } from 'components/DetailsPanel/DetailsHeader/DetailsHeader';
import { DetailsPanelState } from 'components/DetailsPanel/types';
import { SemconvOldNameBadge } from 'components/Semconv';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
@@ -108,6 +109,12 @@ function SpanDetailsContent({
() => getSpanDisplayData(selectedSpan),
[selectedSpan],
);
const semconvLabelSuffix = useCallback(
(fieldKey: string): React.ReactNode => (
<SemconvOldNameBadge name={fieldKey} />
),
[],
);
// Map span attribute actions to PrettyView actions format.
// Use the last key in fieldKeyPath (the actual attribute key), not the full display path.
@@ -329,6 +336,7 @@ function SpanDetailsContent({
visibleActions: VISIBLE_ACTIONS,
pinnedFieldsValue,
onPinnedFieldsChange,
labelSuffixRenderer: semconvLabelSuffix,
}}
/>
</TabsContent>

View File

@@ -29,7 +29,7 @@ export const KEY_ATTRIBUTE_KEYS: Record<string, string[]> = {
traces: [
'service.name',
'service.namespace',
'deployment.environment',
'deployment.environment.name',
'timestamp',
'duration_nano',
'kind_string',

View File

@@ -22,7 +22,7 @@ export const SPAN_CATEGORIES: readonly SpanCategory[] = [
// Map each category to the attribute key it filters on
const CATEGORY_KEYS: Record<Exclude<SpanCategory, 'All'>, string> = {
Database: 'db.system',
Database: 'db.system.name',
HTTP: 'http.method',
Functions: 'kind_string',
Jobs: 'messaging.system',
@@ -34,7 +34,7 @@ const ALL_CATEGORY_KEYS = Object.values(CATEGORY_KEYS);
// The expression clause to add for each category
const CATEGORY_EXPRESSIONS: Record<Exclude<SpanCategory, 'All'>, string> = {
Database: 'db.system exists',
Database: 'db.system.name exists',
HTTP: 'http.method exists',
Functions: "kind_string = 'Internal'",
Jobs: 'messaging.system exists',

View File

@@ -38,7 +38,7 @@ export function Section(props: SectionProps): JSX.Element {
'hasError',
'durationNano',
'serviceName',
'deployment.environment',
'deployment.environment.name',
]),
),
[selectedFilters],

View File

@@ -14,7 +14,7 @@ export const AllTraceFilterKeyValue: Record<string, string> = {
durationNano: 'Duration',
duration_nano: 'Duration',
durationNanoMax: 'Duration',
'deployment.environment': 'Environment',
'deployment.environment.name': 'Environment',
hasError: 'Status',
has_error: 'Status',
serviceName: 'Service Name',
@@ -208,11 +208,11 @@ export const traceFilterKeys: Record<AllTraceFilterKeys, BaseAutocompleteData> =
id: 'serviceName--string--tag--true',
},
'deployment.environment': {
key: 'deployment.environment',
'deployment.environment.name': {
key: 'deployment.environment.name',
dataType: DataTypes.String,
type: 'resource',
id: 'deployment.environment--string--resource--false',
id: 'deployment.environment.name--string--resource--false',
},
name: {
key: 'name',

View File

@@ -223,6 +223,12 @@
padding-left: 6px !important;
}
&__label {
display: inline-flex;
align-items: baseline;
gap: 6px;
}
&__pinned-icon {
flex-shrink: 0;
color: var(--text-robin-400);

View File

@@ -67,6 +67,7 @@ export interface PrettyViewProps {
*/
pinnedFieldsValue?: string[];
onPinnedFieldsChange?: (next: string[]) => void;
labelSuffixRenderer?: (fieldKey: string) => React.ReactNode;
}
function PrettyView({
@@ -78,6 +79,7 @@ function PrettyView({
drawerKey = 'default',
pinnedFieldsValue,
onPinnedFieldsChange,
labelSuffixRenderer,
}: PrettyViewProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [, setCopy] = useCopyToClipboard();
@@ -305,10 +307,24 @@ function PrettyView({
}}
/>
<span>{displayKey}</span>
{labelSuffixRenderer?.(displayKey)}
</span>
);
},
[togglePin, pinnedEntries],
[togglePin, pinnedEntries, labelSuffixRenderer],
);
const labelRenderer = useCallback(
(keyPath: KeyPath): React.ReactNode => {
const displayKey = String(keyPath[0]);
return (
<span className="pretty-view__label">
<span>{displayKey}</span>
{labelSuffixRenderer?.(displayKey)}
</span>
);
},
[labelSuffixRenderer],
);
return (
@@ -351,6 +367,7 @@ function PrettyView({
shouldExpandNodeInitially={shouldExpandNodeInitially}
valueRenderer={valueRenderer}
getItemString={getItemString}
labelRenderer={labelRenderer}
/>
</div>
);

View File

@@ -0,0 +1,14 @@
export interface SemconvMigrationReportEntry {
current: string;
old: string;
signal: string;
services: string[];
resourceSets: number;
lastSeenUnixMilli: number;
}
export interface SemconvMigrationReport {
startUnixMilli: number;
endUnixMilli: number;
entries: SemconvMigrationReportEntry[];
}

View File

@@ -648,3 +648,176 @@ describe('getQueryContextAtCursor - trailing dot in key/value', () => {
expect(ctx.keyToken).toBe('k8s.namespace');
});
});
describe('getQueryContextAtCursor - partial operator', () => {
it('treats text after an incomplete key as an operator prefix', () => {
const q = 'service.name c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('c');
expect(ctx.currentPair).toStrictEqual(
expect.objectContaining({
key: 'service.name',
operator: 'c',
position: expect.objectContaining({
operatorStart: 13,
operatorEnd: 13,
}),
}),
);
});
it('keeps the operator context while completing contains', () => {
const q = 'service.name cont';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('cont');
});
it('treats cursor mid-token as operator context', () => {
const q = 'service.name cont';
// cursor sits between "con" and "t" — user still typing the operator
const ctx = getQueryContextAtCursor(q, 15);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('cont');
});
it('keeps operator context when an AND conjunction precedes the pair', () => {
const q = 'a = 1 AND service.name c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('c');
expect(ctx.currentPair).toStrictEqual(
expect.objectContaining({
key: 'service.name',
operator: 'c',
position: expect.objectContaining({
operatorStart: 23,
operatorEnd: 23,
}),
}),
);
});
it('keeps operator context when an open parenthesis precedes the pair', () => {
const q = '(service.name c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('c');
});
it('re-glues a partial operator that follows a NOT negation', () => {
const q = 'service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.isInKey).toBe(false);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT c');
// operatorStart points at the partial operator (post-NOT), not at the
// negation — so suggestion selection only replaces the partial, never
// the user's typed NOT.
expect(ctx.currentPair).toStrictEqual(
expect.objectContaining({
key: 'service.name',
operator: 'NOT c',
hasNegation: true,
position: expect.objectContaining({
negationStart: 13,
negationEnd: 15,
operatorStart: 17,
operatorEnd: 17,
}),
}),
);
});
it('re-glues a multi-character partial operator after NOT', () => {
const q = 'service.name NOT lik';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT lik');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('re-glues an uppercase partial operator after NOT', () => {
const q = 'service.name NOT EXI';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT EXI');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('preserves original NOT casing in the operator text', () => {
const q = 'service.name not c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('not c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('tolerates extra whitespace between NOT and the partial operator', () => {
const q = 'service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
// Display text uses a canonical single space between NOT and the
// partial, regardless of how many spaces the user typed.
expect(ctx.operatorToken).toBe('NOT c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('keeps operator context for NOT-prefixed partial inside parentheses', () => {
const q = '(service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('keeps operator context for NOT-prefixed partial after an AND conjunction', () => {
const q = 'a = 1 AND service.name NOT c';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('service.name');
expect(ctx.operatorToken).toBe('NOT c');
expect(ctx.currentPair?.hasNegation).toBe(true);
});
it('re-glues the most recent incomplete pair when three partial tokens are typed', () => {
// Pins documented behavior: with two trailing partial pairs (`c` and
// `k`), the heuristic pairs the most recent two — `c` becomes the
// key, `k` becomes the partial operator. The earlier `service.name`
// is dropped from the current pair view.
const q = 'service.name c k';
const ctx = getQueryContextAtCursor(q, q.length);
expect(ctx.isInOperator).toBe(true);
expect(ctx.keyToken).toBe('c');
expect(ctx.operatorToken).toBe('k');
});
});

View File

@@ -0,0 +1,29 @@
import { findOldSemconvNames, getSemconvRename } from 'utils/semconv';
describe('semantic convention helpers', () => {
it('returns the current name for an old attribute', () => {
expect(getSemconvRename('deployment.environment')).toMatchObject({
old: 'deployment.environment',
current: 'deployment.environment.name',
});
});
it('finds old names in editor text without matching larger custom names', () => {
expect(
findOldSemconvNames(
"deployment.environment = 'prod' AND custom.db.system.value = 'x'",
),
).toStrictEqual([
expect.objectContaining({
old: 'deployment.environment',
current: 'deployment.environment.name',
}),
]);
});
it('does not warn for current names', () => {
expect(
findOldSemconvNames('deployment.environment.name = prod'),
).toStrictEqual([]);
});
});

View File

@@ -605,6 +605,98 @@ export function getQueryContextAtCursor(
queryPairs,
);
// Re-glue a partial operator that ANTLR has lexed as a second key.
//
// When the user types `service.name c` (or `service.name NOT c`), the
// lexer sees two KEY tokens (`service.name`, `c`) instead of a key +
// partial operator, so `extractQueryPairs` emits two consecutive
// key-only incomplete pairs. Downstream, that makes the dropdown
// suggest keys when it should be suggesting operators.
//
// Detect that pattern — a previous incomplete key-only pair followed
// by another incomplete key-only `currentPair`, separated only by
// whitespace (or by a negation token attached to the previous pair) —
// and rebuild a single synthetic pair where the previous pair's key
// is the key and the current pair's key is treated as the partial
// operator. The synthetic pair inherits the previous pair's negation
// flag and positions via the spread, so `NOT <partial>` propagates
// correctly to consumers.
const previousIncompletePair = queryPairs
.filter(
(pair) =>
!pair.isComplete &&
!!pair.key &&
!pair.operator &&
pair.position.keyEnd < (currentPair?.position.keyStart ?? cursorIndex),
)
.sort((a, b) => b.position.keyEnd - a.position.keyEnd)[0];
if (
previousIncompletePair &&
currentPair &&
currentPair !== previousIncompletePair &&
!currentPair.operator &&
currentPair.position.keyStart > previousIncompletePair.position.keyEnd
) {
const negationStart = previousIncompletePair.position.negationStart ?? 0;
const negationEnd = previousIncompletePair.position.negationEnd ?? 0;
const negationAfterKey =
previousIncompletePair.hasNegation &&
negationStart > previousIncompletePair.position.keyEnd;
const gapStart = negationAfterKey
? negationEnd + 1
: previousIncompletePair.position.keyEnd + 1;
const textBetweenPairs = query.slice(
gapStart,
currentPair.position.keyStart,
);
if (textBetweenPairs.trim() === '') {
// The replacement range (operatorStart/operatorEnd) must point
// at the partial operator only, NOT the leading negation.
// Consumers like QuerySearch use it to splice the chosen
// suggestion in-place, so including the negation would let a
// `NOT lik` -> `LIKE` selection erase the user's typed `NOT`.
// Matches the convention used for complete pairs in
// extractQueryPairs, where operatorStart starts after the
// negation token.
const operatorStart = currentPair.position.keyStart;
const operatorEnd = currentPair.position.keyEnd;
const partialOperator = query.slice(operatorStart, operatorEnd + 1);
const operatorText = negationAfterKey
? `${query.slice(negationStart, negationEnd + 1)} ${partialOperator}`
: partialOperator;
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: operatorText,
isInKey: false,
isInNegation: false,
isInOperator: true,
isInValue: false,
isInConjunction: false,
isInFunction: false,
isInParenthesis: false,
isInBracketList: false,
keyToken: previousIncompletePair.key,
operatorToken: operatorText,
queryPairs,
currentPair: {
...previousIncompletePair,
operator: operatorText,
position: {
...previousIncompletePair.position,
operatorStart,
operatorEnd,
},
},
};
}
}
// Check if cursor is within any of the specific context boundaries
// FIXED: Include the case where the cursor is exactly at the end of a boundary
const isInKeyBoundary =

View File

@@ -0,0 +1,50 @@
import {
SEMCONV_FAMILIES,
SemconvFamily,
} from 'constants/generated/semconvFamilies.gen';
export type SemconvRename = {
old: string;
current: string;
family: SemconvFamily;
};
const OLD_NAMES = SEMCONV_FAMILIES.flatMap((family) =>
family.old.map((old) => ({ old, current: family.current, family })),
);
const OLD_NAME_INDEX = new Map(OLD_NAMES.map((rename) => [rename.old, rename]));
export function getSemconvRename(name: string): SemconvRename | undefined {
return OLD_NAME_INDEX.get(name);
}
export function findOldSemconvNames(text: string): SemconvRename[] {
if (!text) {
return [];
}
return OLD_NAMES.filter(({ old }) => containsSemconvName(text, old));
}
function containsSemconvName(text: string, name: string): boolean {
let offset = 0;
while (offset < text.length) {
const index = text.indexOf(name, offset);
if (index === -1) {
return false;
}
const before = index === 0 ? '' : text[index - 1];
const afterIndex = index + name.length;
const after = afterIndex === text.length ? '' : text[afterIndex];
if (!isSemconvNameCharacter(before) && !isSemconvNameCharacter(after)) {
return true;
}
offset = index + 1;
}
return false;
}
function isSemconvNameCharacter(value: string): boolean {
return /[A-Za-z0-9_.-]/.test(value);
}

View File

@@ -46,5 +46,23 @@ func (provider *provider) addFieldsRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/fields/semconv-migration", handler.New(provider.authzMiddleware.ViewAccess(provider.fieldsHandler.GetSemconvMigrationReport), handler.OpenAPIDef{
ID: "GetSemconvMigrationReport",
Tags: []string{"fields"},
Summary: "Get semantic-convention migration report",
Description: "Returns services that still emit old semantic-convention names without the current family name",
Request: nil,
RequestQuery: new(telemetrytypes.PostableSemconvMigrationReportParams),
RequestContentType: "",
Response: new(telemetrytypes.GettableSemconvMigrationReport),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -8,4 +8,7 @@ type Handler interface {
// Gets the fields values for the given field value selector
GetFieldsValues(http.ResponseWriter, *http.Request)
// Gets services that still emit only historical semantic-convention names.
GetSemconvMigrationReport(http.ResponseWriter, *http.Request)
}

View File

@@ -2,6 +2,7 @@ package implfields
import (
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
@@ -16,6 +17,43 @@ type handler struct {
telemetryMetadataStore telemetrytypes.MetadataStore
}
func (handler *handler) GetSemconvMigrationReport(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
var params telemetrytypes.PostableSemconvMigrationReportParams
if err := binding.Query.BindQuery(req.URL.Query(), &params); err != nil {
render.Error(rw, err)
return
}
now := time.Now()
if params.EndUnixMilli == 0 {
params.EndUnixMilli = now.UnixMilli()
}
if params.StartUnixMilli == 0 {
params.StartUnixMilli = now.Add(-24 * time.Hour).UnixMilli()
}
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
report, err := handler.telemetryMetadataStore.GetSemconvMigrationReport(
ctx,
valuer.MustNewUUID(claims.OrgID),
params.StartUnixMilli,
params.EndUnixMilli,
)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, report)
}
func NewHandler(settings factory.ProviderSettings, telemetryMetadataStore telemetrytypes.MetadataStore) fields.Handler {
return &handler{
telemetryMetadataStore: telemetryMetadataStore,

View File

@@ -786,10 +786,11 @@ func (q *querier) run(
Results: maps.Values(processedResults),
},
Meta: qbtypes.ExecStats{
RowsScanned: stats.RowsScanned,
BytesScanned: stats.BytesScanned,
DurationMS: stats.DurationMS,
StepIntervals: stepIntervals,
RowsScanned: stats.RowsScanned,
BytesScanned: stats.BytesScanned,
DurationMS: stats.DurationMS,
StepIntervals: stepIntervals,
SemconvResolutions: semconvResolutionsForRequest(req),
},
}

View File

@@ -0,0 +1,128 @@
package querier
import (
"encoding/json"
"slices"
"strings"
"unicode"
"github.com/SigNoz/signoz/pkg/semconv"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
// semconvResolutionsForRequest reports only query-builder resolutions. Raw
// ClickHouse SQL and PromQL are deliberately excluded: SigNoz does not rewrite
// those languages and their editors surface non-blocking warnings instead.
func semconvResolutionsForRequest(req *qbtypes.QueryRangeRequest) []qbtypes.SemconvResolution {
if req == nil {
return nil
}
resolutions := make([]qbtypes.SemconvResolution, 0)
seen := make(map[string]struct{})
for _, envelope := range req.CompositeQuery.Queries {
signal, applies := semconvResolutionSignal(envelope.Spec)
if !applies {
continue
}
payload, err := json.Marshal(envelope.Spec)
if err != nil {
continue
}
text := string(payload)
for _, family := range semconv.All() {
if _, ok := semconv.Lookup(family.Kind, telemetrytypes.FieldKeySelector{
Name: family.Current,
Signal: signal,
}); !ok {
continue
}
for _, requested := range semconvRequestSpellings(family, signal) {
if !containsSemconvName(text, requested) {
continue
}
identity := family.Kind.StringValue() + "\x00" + requested + "\x00" + family.Current
if _, ok := seen[identity]; ok {
continue
}
seen[identity] = struct{}{}
resolutions = append(resolutions, qbtypes.SemconvResolution{
Requested: requested,
Current: family.Current,
Members: append([]string{family.Current}, family.Old...),
Kind: family.Kind.StringValue(),
})
}
}
}
if len(resolutions) == 0 {
return nil
}
return resolutions
}
func semconvResolutionSignal(spec any) (telemetrytypes.Signal, bool) {
switch spec.(type) {
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation], qbtypes.QueryBuilderTraceOperator:
return telemetrytypes.SignalTraces, true
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
return telemetrytypes.SignalLogs, true
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
return telemetrytypes.SignalMetrics, true
case qbtypes.QueryBuilderJoin:
// Joins can contain more than one signal. An unspecified signal keeps
// family signal scopes in force while allowing every applicable family.
return telemetrytypes.SignalUnspecified, true
default:
return telemetrytypes.SignalUnspecified, false
}
}
func semconvRequestSpellings(family semconv.Family, signal telemetrytypes.Signal) []string {
spellings := append([]string{family.Current}, family.Old...)
if signal != telemetrytypes.SignalMetrics {
return spellings
}
logical := slices.Clone(spellings)
for _, name := range logical {
normalized := strings.ReplaceAll(name, ".", "_")
if !slices.Contains(spellings, normalized) {
spellings = append(spellings, normalized)
}
if family.Kind == semconv.KindAttribute {
for _, resourceName := range []string{"resource_" + name, "resource_" + normalized} {
if !slices.Contains(spellings, resourceName) {
spellings = append(spellings, resourceName)
}
}
}
}
return spellings
}
func containsSemconvName(text, name string) bool {
for offset := 0; offset < len(text); {
index := strings.Index(text[offset:], name)
if index < 0 {
return false
}
start := offset + index
end := start + len(name)
if (start == 0 || !isSemconvNameRune(rune(text[start-1]))) &&
(end == len(text) || !isSemconvNameRune(rune(text[end]))) {
return true
}
offset = start + 1
}
return false
}
func isSemconvNameRune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '.' || r == '-'
}

View File

@@ -0,0 +1,70 @@
package querier
import (
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func TestSemconvResolutionsReportsOldTraceAttribute(t *testing.T) {
req := &qbtypes.QueryRangeRequest{
CompositeQuery: qbtypes.CompositeQuery{Queries: []qbtypes.QueryEnvelope{{
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "deployment.environment = 'prod'"},
},
}}},
}
assert.Equal(t, []qbtypes.SemconvResolution{{
Requested: "deployment.environment",
Current: "deployment.environment.name",
Members: []string{"deployment.environment.name", "deployment.environment"},
Kind: "attribute",
}}, semconvResolutionsForRequest(req), "old trace attribute should be reported as a family resolution")
}
func TestSemconvResolutionsReportsCurrentLogAttribute(t *testing.T) {
req := &qbtypes.QueryRangeRequest{
CompositeQuery: qbtypes.CompositeQuery{Queries: []qbtypes.QueryEnvelope{{
Spec: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
SelectFields: []telemetrytypes.TelemetryFieldKey{{
Name: "db.system.name",
}},
},
}}},
}
assert.Equal(t, []qbtypes.SemconvResolution{{
Requested: "db.system.name",
Current: "db.system.name",
Members: []string{"db.system.name", "db.system"},
Kind: "attribute",
}}, semconvResolutionsForRequest(req), "current log attribute should identify its complete family")
}
func TestSemconvResolutionsIgnoresRawSQL(t *testing.T) {
req := &qbtypes.QueryRangeRequest{
CompositeQuery: qbtypes.CompositeQuery{Queries: []qbtypes.QueryEnvelope{{
Spec: qbtypes.ClickHouseQuery{Query: "SELECT attributes_string['deployment.environment']"},
}}},
}
assert.Empty(t, semconvResolutionsForRequest(req), "raw SQL is not rewritten and should not report a resolution")
}
func TestSemconvResolutionsRequiresNameBoundary(t *testing.T) {
req := &qbtypes.QueryRangeRequest{
CompositeQuery: qbtypes.CompositeQuery{Queries: []qbtypes.QueryEnvelope{{
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{Expression: "custom.deployment.environment = 'prod'"},
},
}}},
}
assert.Empty(t, semconvResolutionsForRequest(req), "a family name embedded in a larger custom key must not match")
}

View File

@@ -52,10 +52,10 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/constants"
chErrors "github.com/SigNoz/signoz/pkg/query-service/errors"
"github.com/SigNoz/signoz/pkg/query-service/metrics"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/semconv"
)
const (
@@ -3190,11 +3190,6 @@ func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, orgID v
var rows driver.Rows
var attributeValues v3.FilterAttributeValueResponse
normalized := true
if constants.IsDotMetricsEnabled {
normalized = false
}
reductionEnabled := r.fl.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, featuretypes.NewFlaggerEvaluationContext(orgID))
if reductionEnabled {
@@ -3205,8 +3200,7 @@ func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, orgID v
if req.Limit != 0 {
query = query + fmt.Sprintf(" LIMIT %d;", req.Limit)
}
names := []string{req.AggregateAttribute}
names = append(names, metrics.GetTransitionedMetric(req.AggregateAttribute, normalized))
names := semconv.MetricNames(req.AggregateAttribute)
rows, err = r.db.Query(ctx, query, req.FilterAttributeKey, names, req.FilterAttributeKey, fmt.Sprintf("%%%s%%", req.SearchText), common.PastDayRoundOff())

View File

@@ -6,6 +6,8 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
var resourceLogOperators = map[v3.FilterOperator]string{
@@ -29,13 +31,61 @@ var resourceLogOperators = map[v3.FilterOperator]string{
v3.FilterOperatorNotILike: "NOT ILIKE",
}
func resourceSemconvMembers(key string) []string {
return semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
Name: key,
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
})
}
func resourceValueExpression(key string) string {
members := resourceSemconvMembers(key)
if len(members) == 1 {
return fmt.Sprintf("simpleJSONExtractString(labels, '%s')", key)
}
values := make([]string, 0, len(members))
for _, member := range members {
values = append(values, fmt.Sprintf("NULLIF(simpleJSONExtractString(labels, '%s'), '')", member))
}
return "COALESCE(" + strings.Join(values, ", ") + ")"
}
func resourcePresenceExpression(key string, exists bool) string {
members := resourceSemconvMembers(key)
if len(members) == 1 {
if exists {
return fmt.Sprintf("simpleJSONHas(labels, '%s')", key)
}
return fmt.Sprintf("not simpleJSONHas(labels, '%s')", key)
}
conditions := make([]string, 0, len(members))
for _, member := range members {
if exists {
conditions = append(conditions, fmt.Sprintf("simpleJSONHas(labels, '%s')", member))
} else {
conditions = append(conditions, fmt.Sprintf("not simpleJSONHas(labels, '%s')", member))
}
}
separator := " OR "
if !exists {
separator = " AND "
}
return "(" + strings.Join(conditions, separator) + ")"
}
// buildResourceFilter builds a clickhouse filter string for resource labels
func buildResourceFilter(logsOp string, key string, op v3.FilterOperator, value interface{}) string {
// for all operators except contains and like
searchKey := fmt.Sprintf("simpleJSONExtractString(labels, '%s')", key)
searchKey := resourceValueExpression(key)
// for contains and like it will be case insensitive
lowerSearchKey := fmt.Sprintf("simpleJSONExtractString(lower(labels), '%s')", key)
if len(resourceSemconvMembers(key)) > 1 {
lowerSearchKey = "lower(" + searchKey + ")"
}
chFmtVal := utils.ClickHouseFormattedValue(value)
@@ -43,9 +93,9 @@ func buildResourceFilter(logsOp string, key string, op v3.FilterOperator, value
switch op {
case v3.FilterOperatorExists:
return fmt.Sprintf("simpleJSONHas(labels, '%s')", key)
return resourcePresenceExpression(key, true)
case v3.FilterOperatorNotExists:
return fmt.Sprintf("not simpleJSONHas(labels, '%s')", key)
return resourcePresenceExpression(key, false)
case v3.FilterOperatorRegex, v3.FilterOperatorNotRegex:
return fmt.Sprintf(logsOp, searchKey, chFmtVal)
case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
@@ -110,6 +160,38 @@ func buildIndexFilterForInOperator(key string, op v3.FilterOperator, value inter
// we can use lower index for =, in etc but it's difficult to do it for !=, NIN etc
// if as x != "ABC" we cannot predict something like "not lower(labels) like '%%x%%abc%%'". It has it be "not lower(labels) like '%%x%%ABC%%'"
func buildResourceIndexFilter(key string, op v3.FilterOperator, value interface{}) string {
return buildResourceIndexFilterForKey(key, op, value, true)
}
func buildResourceIndexFilterForKey(key string, op v3.FilterOperator, value interface{}, resolveFamily bool) string {
members := []string{key}
if resolveFamily {
members = resourceSemconvMembers(key)
}
if len(members) > 1 {
switch op {
case v3.FilterOperatorNotEqual,
v3.FilterOperatorNotLike,
v3.FilterOperatorNotILike,
v3.FilterOperatorNotContains,
v3.FilterOperatorNotExists,
v3.FilterOperatorNotRegex,
v3.FilterOperatorNotIn:
return ""
}
conditions := make([]string, 0, len(members))
for _, member := range members {
if condition := buildResourceIndexFilterForKey(member, op, value, false); condition != "" {
conditions = append(conditions, condition)
}
}
if len(conditions) == 0 {
return ""
}
return "(" + strings.Join(conditions, " OR ") + ")"
}
// not using clickhouseFormattedValue as we don't wan't the quotes
strVal := fmt.Sprintf("%s", value)
fmtValEscapedForContains := utils.QuoteEscapedStringForContains(strVal, true)
@@ -206,14 +288,31 @@ func buildResourceFiltersFromGroupBy(groupBy []v3.AttributeKey) []string {
if attr.Type != v3.AttributeKeyTypeResource {
continue
}
conditions = append(conditions, fmt.Sprintf("(simpleJSONHas(labels, '%s') AND labels like '%%%s%%')", attr.Key, attr.Key))
members := resourceSemconvMembers(attr.Key)
if len(members) == 1 {
conditions = append(conditions, fmt.Sprintf("(simpleJSONHas(labels, '%s') AND labels like '%%%s%%')", attr.Key, attr.Key))
continue
}
indexConditions := make([]string, 0, len(members))
for _, member := range members {
indexConditions = append(indexConditions, fmt.Sprintf("labels like '%%%s%%'", member))
}
conditions = append(conditions, fmt.Sprintf("(%s AND (%s))", resourcePresenceExpression(attr.Key, true), strings.Join(indexConditions, " OR ")))
}
return conditions
}
func buildResourceFiltersFromAggregateAttribute(aggregateAttribute v3.AttributeKey) string {
if aggregateAttribute.Key != "" && aggregateAttribute.Type == v3.AttributeKeyTypeResource {
return fmt.Sprintf("(simpleJSONHas(labels, '%s') AND labels like '%%%s%%')", aggregateAttribute.Key, aggregateAttribute.Key)
members := resourceSemconvMembers(aggregateAttribute.Key)
if len(members) == 1 {
return fmt.Sprintf("(simpleJSONHas(labels, '%s') AND labels like '%%%s%%')", aggregateAttribute.Key, aggregateAttribute.Key)
}
indexConditions := make([]string, 0, len(members))
for _, member := range members {
indexConditions = append(indexConditions, fmt.Sprintf("labels like '%%%s%%'", member))
}
return fmt.Sprintf("(%s AND (%s))", resourcePresenceExpression(aggregateAttribute.Key, true), strings.Join(indexConditions, " OR "))
}
return ""

View File

@@ -5,6 +5,8 @@ import (
"testing"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_buildResourceFilter(t *testing.T) {
@@ -552,3 +554,38 @@ func Test_buildResourceSubQuery(t *testing.T) {
})
}
}
func TestSemanticConventionResourceFamily(t *testing.T) {
const resolvedValue = "COALESCE(NULLIF(simpleJSONExtractString(labels, 'deployment.environment.name'), ''), NULLIF(simpleJSONExtractString(labels, 'deployment.environment'), ''))"
for _, requestedName := range []string{"deployment.environment.name", "deployment.environment"} {
t.Run(requestedName, func(t *testing.T) {
assert.Equal(t, resolvedValue+" = 'production'", buildResourceFilter("=", requestedName, v3.FilterOperatorEqual, "production"))
assert.Equal(t, "(simpleJSONHas(labels, 'deployment.environment.name') OR simpleJSONHas(labels, 'deployment.environment'))", buildResourceFilter("", requestedName, v3.FilterOperatorExists, nil))
assert.Equal(t, "(not simpleJSONHas(labels, 'deployment.environment.name') AND not simpleJSONHas(labels, 'deployment.environment'))", buildResourceFilter("", requestedName, v3.FilterOperatorNotExists, nil))
assert.Equal(t, "(labels like '%deployment.environment.name\":\"production%' OR labels like '%deployment.environment\":\"production%')", buildResourceIndexFilter(requestedName, v3.FilterOperatorEqual, "production"))
assert.Empty(t, buildResourceIndexFilter(requestedName, v3.FilterOperatorNotEqual, "production"), "negative family filter must not use a rejecting index hint")
})
}
filters, err := buildResourceFiltersFromFilterItems(&v3.FilterSet{Items: []v3.FilterItem{{
Key: v3.AttributeKey{
Key: "deployment.environment.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeResource,
},
Operator: v3.FilterOperatorEqual,
Value: "production",
}}})
require.NoError(t, err, "family filter items must build before their output is inspected")
wantFilters := []string{
resolvedValue + " = 'production'",
"(labels like '%deployment.environment.name\":\"production%' OR labels like '%deployment.environment\":\"production%')",
}
assert.Equal(t, wantFilters, filters)
wantPresence := "((simpleJSONHas(labels, 'deployment.environment.name') OR simpleJSONHas(labels, 'deployment.environment')) AND (labels like '%deployment.environment.name%' OR labels like '%deployment.environment%'))"
groupBy := buildResourceFiltersFromGroupBy([]v3.AttributeKey{{Key: "deployment.environment", Type: v3.AttributeKeyTypeResource}})
assert.Equal(t, []string{wantPresence}, groupBy)
assert.Equal(t, wantPresence, buildResourceFiltersFromAggregateAttribute(v3.AttributeKey{Key: "deployment.environment.name", Type: v3.AttributeKeyTypeResource}))
}

View File

@@ -6,16 +6,32 @@ import (
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
var (
columns = map[string]struct{}{
"deployment_environment": {},
"k8s_cluster_name": {},
"k8s_namespace_name": {},
}
columns = serviceMapColumns()
)
func serviceMapColumns() map[string]string {
columns := map[string]string{
"k8s_cluster_name": "k8s_cluster_name",
"k8s_namespace_name": "k8s_namespace_name",
}
// Dependency-graph rows keep their historical physical column name. Both
// semantic-convention request spellings target that same derived column.
for _, member := range semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
Name: "deployment.environment.name",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
}) {
columns[strings.ReplaceAll(member, ".", "_")] = "deployment_environment"
}
return columns
}
func BuildServiceMapQuery(tags []model.TagQuery) (string, []interface{}) {
var filterQuery string
var namedArgs []interface{}
@@ -24,39 +40,40 @@ func BuildServiceMapQuery(tags []model.TagQuery) (string, []interface{}) {
operator := tag.GetOperator()
value := tag.GetValues()
if _, ok := columns[key]; !ok {
column, ok := columns[key]
if !ok {
continue
}
switch operator {
case model.InOperator:
filterQuery += fmt.Sprintf(" AND %s IN @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s IN @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, value))
case model.NotInOperator:
filterQuery += fmt.Sprintf(" AND %s NOT IN @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s NOT IN @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, value))
case model.EqualOperator:
filterQuery += fmt.Sprintf(" AND %s = @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s = @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, value))
case model.NotEqualOperator:
filterQuery += fmt.Sprintf(" AND %s != @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s != @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, value))
case model.ContainsOperator:
filterQuery += fmt.Sprintf(" AND %s LIKE @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s LIKE @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%%%s%%", value)))
case model.NotContainsOperator:
filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%%%s%%", value)))
case model.StartsWithOperator:
filterQuery += fmt.Sprintf(" AND %s LIKE @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s LIKE @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%s%%", value)))
case model.NotStartsWithOperator:
filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", key, key)
filterQuery += fmt.Sprintf(" AND %s NOT LIKE @%s", column, key)
namedArgs = append(namedArgs, clickhouse.Named(key, fmt.Sprintf("%s%%", value)))
case model.ExistsOperator:
filterQuery += fmt.Sprintf(" AND %s IS NOT NULL", key)
filterQuery += fmt.Sprintf(" AND %s IS NOT NULL", column)
case model.NotExistsOperator:
filterQuery += fmt.Sprintf(" AND %s IS NULL", key)
filterQuery += fmt.Sprintf(" AND %s IS NULL", column)
}
}
return filterQuery, namedArgs

View File

@@ -0,0 +1,35 @@
package services
import (
"testing"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBuildServiceMapQueryAcceptsEnvironmentFamily(t *testing.T) {
for _, requestedName := range []string{"deployment.environment.name", "deployment.environment"} {
t.Run(requestedName, func(t *testing.T) {
tags := []model.TagQuery{model.NewTagQueryString(model.TagQueryParam{
Key: requestedName,
StringValues: []string{"production"},
Operator: model.InOperator,
TagType: model.ResourceAttributeTagType,
})}
query, args := BuildServiceMapQuery(tags)
argName := "deployment_environment"
if requestedName == "deployment.environment.name" {
argName = "deployment_environment_name"
}
assert.Equal(t, " AND deployment_environment IN @"+argName, query)
require.Len(t, args, 1)
named, ok := args[0].(driver.NamedValue)
require.True(t, ok)
assert.Equal(t, argName, named.Name)
assert.Equal(t, []interface{}{"production"}, named.Value)
})
}
}

View File

@@ -1,27 +0,0 @@
package metrics
var MetricsUnderTransition = map[string]string{
"k8s_pod_cpu_utilization": "k8s_pod_cpu_usage",
"k8s_node_cpu_utilization": "k8s_node_cpu_usage",
"container_cpu_utilization": "container_cpu_usage",
}
var DotMetricsUnderTransition = map[string]string{
"k8s.pod.cpu.utilization": "k8s.pod.cpu.usage",
"k8s.node.cpu.utilization": "k8s.node.cpu.usage",
"container.cpu.utilization": "container.cpu.usage",
}
func GetTransitionedMetric(metric string, normalized bool) string {
if normalized {
if _, ok := MetricsUnderTransition[metric]; ok {
return MetricsUnderTransition[metric]
}
return metric
} else {
if _, ok := DotMetricsUnderTransition[metric]; ok {
return DotMetricsUnderTransition[metric]
}
return metric
}
}

View File

@@ -10,8 +10,8 @@ import (
"log/slog"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/metrics"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/semconv"
)
// ValidateAndCastValue validates and casts the value of a key to the corresponding data type of the key
@@ -234,12 +234,12 @@ func ClickHouseFormattedValue(v interface{}) string {
func ClickHouseFormattedMetricNames(v interface{}) string {
if name, ok := v.(string); ok {
transitionedMetrics := metrics.GetTransitionedMetric(name, !constants.IsDotMetricsEnabled)
if transitionedMetrics != name {
return ClickHouseFormattedValue([]interface{}{transitionedMetrics})
} else {
return ClickHouseFormattedValue([]interface{}{name})
members := semconv.MetricNames(name)
values := make([]interface{}, 0, len(members))
for _, member := range members {
values = append(values, member)
}
return ClickHouseFormattedValue(values)
}
return ClickHouseFormattedValue(v)

View File

@@ -2,13 +2,29 @@ package querybuilder
import (
"fmt"
"strings"
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/semconv"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
func physicalSemconvMembers(key *telemetrytypes.TelemetryFieldKey) []string {
if len(key.SemconvMembers) > 0 {
return key.SemconvMembers
}
if key.FieldContext != telemetrytypes.FieldContextResource && key.FieldContext != telemetrytypes.FieldContextAttribute {
return []string{key.Name}
}
return semconv.AttributeMembers(telemetrytypes.FieldKeySelector{
Name: key.Name,
Signal: key.Signal,
FieldContext: key.FieldContext,
})
}
// ExistsExpression renders the existence predicate for a key resolved to the given
// columns (negated when exists is false). Comparisons are against constants rendered
// as literals, so the expression carries no bind args and can guard column expressions
@@ -43,11 +59,26 @@ func ExistsExpression(columns []*schema.Column, key *telemetrytypes.TelemetryFie
if len(evolutionsEntries) > 0 && evolutionsEntries[0] != nil {
columnName = evolutionsEntries[0].ColumnName
}
rawPath := fmt.Sprintf("%s.`%s`", columnName, key.Name)
if exists {
return rawPath + " IS NOT NULL", nil
members := physicalSemconvMembers(key)
paths := make([]string, 0, len(members))
for _, member := range members {
paths = append(paths, fmt.Sprintf("%s.`%s`", columnName, member))
}
return rawPath + " IS NULL", nil
if len(paths) == 1 {
if exists {
return paths[0] + " IS NOT NULL", nil
}
return paths[0] + " IS NULL", nil
}
guards := make([]string, 0, len(paths))
for _, path := range paths {
guards = append(guards, path+" IS NOT NULL")
}
rawPath := "(" + strings.Join(guards, " OR ") + ")"
if exists {
return rawPath, nil
}
return "NOT " + rawPath, nil
case schema.ColumnTypeEnumString,
schema.ColumnTypeEnumFixedString:
if exists {
@@ -88,8 +119,16 @@ func ExistsExpression(columns []*schema.Column, key *telemetrytypes.TelemetryFie
switch valueType := column.Type.(schema.MapColumnType).ValueType; valueType.GetType() {
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumBool, schema.ColumnTypeEnumFloat64:
leftOperand := fmt.Sprintf("mapContains(%s, '%s')", column.Name, key.Name)
if key.Materialized {
members := physicalSemconvMembers(key)
operands := make([]string, 0, len(members))
for _, member := range members {
operands = append(operands, fmt.Sprintf("mapContains(%s, '%s')", column.Name, member))
}
leftOperand := strings.Join(operands, " OR ")
if len(operands) > 1 {
leftOperand = "(" + leftOperand + ")"
}
if key.Materialized && (len(members) == 1 || key.MaterializedSemconv) {
leftOperand = telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
}
if exists {

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
"github.com/SigNoz/signoz/pkg/semconv"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -38,6 +39,7 @@ type filterExpressionVisitor struct {
fullTextColumn *telemetrytypes.TelemetryFieldKey
skipResourceFilter bool
skipFullTextFilter bool
exactSemconv bool
variables map[string]qbtypes.VariableItem
keysWithWarnings map[string]bool
@@ -58,6 +60,7 @@ type FilterExprVisitorOpts struct {
FullTextColumn *telemetrytypes.TelemetryFieldKey
SkipResourceFilter bool
SkipFullTextFilter bool
ExactSemconv bool
Variables map[string]qbtypes.VariableItem
StartNs uint64
EndNs uint64
@@ -75,6 +78,7 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
fullTextColumn: opts.FullTextColumn,
skipResourceFilter: opts.SkipResourceFilter,
skipFullTextFilter: opts.SkipFullTextFilter,
exactSemconv: opts.ExactSemconv,
variables: opts.Variables,
keysWithWarnings: make(map[string]bool),
startNs: opts.StartNs,
@@ -379,7 +383,7 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
// VisitComparison handles all comparison operators.
func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
key := v.Visit(ctx.Key()).(*telemetrytypes.TelemetryFieldKey)
matching := MatchingFieldKeys(key, v.fieldKeys)
matching := v.matchingFieldKeys(key)
// Handle EXISTS specially
if ctx.EXISTS() != nil {
@@ -730,7 +734,7 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
return ErrorConditionLiteral
}
conds, ok := v.buildConditions(key, MatchingFieldKeys(key, v.fieldKeys), operator, value)
conds, ok := v.buildConditions(key, v.matchingFieldKeys(key), operator, value)
if !ok {
return ErrorConditionLiteral
}
@@ -923,7 +927,7 @@ func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
// buildConditions invokes the condition builder for a filter term, folding its
// warnings/errors into visitor state; returns false if an error was recorded.
func (v *filterExpressionVisitor) buildConditions(key *telemetrytypes.TelemetryFieldKey, matching []*telemetrytypes.TelemetryFieldKey, op qbtypes.FilterOperator, value any) ([]string, bool) {
conds, warns, err := v.conditionBuilder.ConditionFor(v.context, v.orgID, v.startNs, v.endNs, key, v.fieldKeys, qbtypes.ConditionBuilderOptions{SkipResourceFilter: v.skipResourceFilter}, op, value, v.builder)
conds, warns, err := v.conditionBuilder.ConditionFor(v.context, v.orgID, v.startNs, v.endNs, key, v.fieldKeys, qbtypes.ConditionBuilderOptions{SkipResourceFilter: v.skipResourceFilter, ExactSemconv: v.exactSemconv}, op, value, v.builder)
if err != nil {
_, _, _, _, errURL, _ := errors.Unwrapb(err)
assignIfEmpty(&v.mainErrorURL, errURL)
@@ -982,27 +986,116 @@ func assignIfEmpty(s *string, value string) {
// MatchingFieldKeys returns the field keys from the map that match the given key,
// honoring any context/data type the user specified.
func MatchingFieldKeys(field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
fieldKeysForName := []*telemetrytypes.TelemetryFieldKey{}
return matchingFieldKeys(field, fieldKeys, true)
}
// match by name; keep items whose context and data type match (unspecified matches any)
for _, item := range fieldKeys[field.Name] {
if (field.FieldContext == telemetrytypes.FieldContextUnspecified || field.FieldContext == item.FieldContext) &&
(field.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || field.FieldDataType == item.FieldDataType) {
fieldKeysForName = append(fieldKeysForName, item)
// MatchingFieldKeysExact matches only the requested physical spelling.
func MatchingFieldKeysExact(field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
return matchingFieldKeys(field, fieldKeys, false)
}
func matchingFieldKeys(field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey, resolveSemconv bool) []*telemetrytypes.TelemetryFieldKey {
selector := telemetrytypes.FieldKeySelector{
Name: field.Name,
Signal: field.Signal,
FieldContext: field.FieldContext,
}
members := []string{field.Name}
if resolveSemconv {
members = semconv.AttributeMembers(selector)
}
isFamily := len(members) > 1
fieldKeysForName := make([]*telemetrytypes.TelemetryFieldKey, 0)
indexByIdentity := make(map[string]int)
appendMatches := func(lookupName string, memberName string, contextAlreadyMatched bool) {
for _, item := range fieldKeys[lookupName] {
if !contextAlreadyMatched && field.FieldContext != telemetrytypes.FieldContextUnspecified && field.FieldContext != item.FieldContext {
continue
}
if field.FieldDataType != telemetrytypes.FieldDataTypeUnspecified && field.FieldDataType != item.FieldDataType {
continue
}
// A wildcard lookup may have found a same-named field in a scope where
// this family does not apply. Keep exact names, but reject cross-member
// matches outside the generated family scope.
if resolveSemconv && memberName != field.Name {
itemSelector := telemetrytypes.FieldKeySelector{
Name: field.Name,
Signal: item.Signal,
FieldContext: item.FieldContext,
}
if !slices.Contains(semconv.AttributeMembers(itemSelector), memberName) {
continue
}
}
physicalMembers := item.SemconvMembers
if len(physicalMembers) == 0 {
physicalMembers = []string{memberName}
}
identity := item.Signal.StringValue() + ";" + item.FieldContext.StringValue() + ";" + item.FieldDataType.StringValue()
if isFamily {
if index, found := indexByIdentity[identity]; found {
for _, physicalMember := range physicalMembers {
if !slices.Contains(fieldKeysForName[index].SemconvMembers, physicalMember) {
fieldKeysForName[index].SemconvMembers = append(fieldKeysForName[index].SemconvMembers, physicalMember)
}
}
continue
}
indexByIdentity[identity] = len(fieldKeysForName)
}
resolved := *item
// The requested spelling is the response identity. Field mappers use
// it to resolve the available family members current-first.
if isFamily {
resolved.Name = field.Name
resolved.SemconvMembers = slices.Clone(physicalMembers)
} else if !resolveSemconv {
resolved.SemconvMembers = []string{field.Name}
}
fieldKeysForName = append(fieldKeysForName, &resolved)
}
}
// A context may have been split off a name that legitimately contained it (e.g.
// `attribute.key`); also look up the context-prefixed name so both readings resolve.
// Members are current-first, so metadata from the current key wins when
// both spellings describe the same signal/context/type.
for _, member := range members {
appendMatches(member, member, false)
}
// A context may have been split off a name that legitimately contained it
// (e.g. `attribute.key`); preserve that historical alternate reading for
// every family member.
if field.FieldContext != telemetrytypes.FieldContextUnspecified {
contextPrefixedFieldName := fmt.Sprintf("%s.%s", field.FieldContext.StringValue(), field.Name)
for _, item := range fieldKeys[contextPrefixedFieldName] {
// Context already matched via the lookup key; only data type needs checking.
if field.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || item.FieldDataType == field.FieldDataType {
fieldKeysForName = append(fieldKeysForName, item)
}
for _, member := range members {
appendMatches(fmt.Sprintf("%s.%s", field.FieldContext.StringValue(), member), member, true)
}
}
return fieldKeysForName
}
func (v *filterExpressionVisitor) matchingFieldKeys(field *telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
if v.exactSemconv {
return MatchingFieldKeysExact(field, v.fieldKeys)
}
return MatchingFieldKeys(field, v.fieldKeys)
}
// ExactSemconvKeys returns copies pinned to their physical names, preventing a
// field mapper from expanding a synthesized or metadata-free key into a family.
func ExactSemconvKeys(keys []*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
result := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))
for _, key := range keys {
if key == nil {
continue
}
resolved := *key
resolved.SemconvMembers = []string{key.Name}
result = append(result, &resolved)
}
return result
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/antlr4-go/antlr/v4"
sqlbuilder "github.com/huandu/go-sqlbuilder"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPrepareWhereClause_EmptyVariableList ensures PrepareWhereClause errors when a variable has an empty list value.
@@ -685,6 +686,58 @@ func TestVisitKey(t *testing.T) {
}
}
func TestMatchingFieldKeysResolvesSemconvFamily(t *testing.T) {
current := &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment.name",
Description: "current metadata",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
old := &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
Description: "old metadata",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
fieldKeys := map[string][]*telemetrytypes.TelemetryFieldKey{
current.Name: {current},
old.Name: {old},
}
for _, requestedName := range []string{current.Name, old.Name} {
requested := telemetrytypes.NewTelemetryFieldKey(
requestedName,
telemetrytypes.FieldContextResource,
telemetrytypes.FieldDataTypeString,
)
matches := MatchingFieldKeys(requested, fieldKeys)
require.Len(t, matches, 1, "family lookup must return one field before its metadata is inspected")
assert.Equal(t, requestedName, matches[0].Name)
assert.Equal(t, "current metadata", matches[0].Description)
assert.Equal(t, []string{current.Name, old.Name}, matches[0].SemconvMembers)
}
// A current-name query still resolves when metadata has seen only the old
// spelling. The returned name remains the request identity.
requested := telemetrytypes.NewTelemetryFieldKey(
current.Name,
telemetrytypes.FieldContextResource,
telemetrytypes.FieldDataTypeString,
)
matches := MatchingFieldKeys(requested, map[string][]*telemetrytypes.TelemetryFieldKey{old.Name: {old}})
require.Len(t, matches, 1, "family lookup must return one field before its metadata is inspected")
assert.Equal(t, current.Name, matches[0].Name)
assert.Equal(t, "old metadata", matches[0].Description)
assert.Equal(t, []string{old.Name}, matches[0].SemconvMembers)
exactMatches := MatchingFieldKeysExact(requested, fieldKeys)
require.Len(t, exactMatches, 1, "exact lookup must return one field before its metadata is inspected")
assert.Equal(t, current.Name, exactMatches[0].Name)
assert.Equal(t, []string{current.Name}, exactMatches[0].SemconvMembers)
}
// ---------------------------------------------------------------------------
// TestVisitComparison
// ---------------------------------------------------------------------------

3
pkg/semconv/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Package semconv resolves historical OpenTelemetry semantic-convention names
// into logical field families.
package semconv

View File

@@ -0,0 +1,48 @@
// Code generated by scripts/semconv. DO NOT EDIT.
package semconv
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
var families = []Family{
{
Current: "container.cpu.usage",
Old: []string{"container.cpu.utilization"},
Kind: KindMetric,
Contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextMetric},
Signals: []telemetrytypes.Signal{telemetrytypes.SignalMetrics},
ApplyToMetrics: nil,
},
{
Current: "db.system.name",
Old: []string{"db.system"},
Kind: KindAttribute,
Contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextResource},
Signals: []telemetrytypes.Signal{telemetrytypes.SignalLogs, telemetrytypes.SignalMetrics, telemetrytypes.SignalTraces},
ApplyToMetrics: nil,
},
{
Current: "deployment.environment.name",
Old: []string{"deployment.environment"},
Kind: KindAttribute,
Contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextAttribute, telemetrytypes.FieldContextResource},
Signals: []telemetrytypes.Signal{telemetrytypes.SignalLogs, telemetrytypes.SignalMetrics, telemetrytypes.SignalTraces},
ApplyToMetrics: nil,
},
{
Current: "k8s.node.cpu.usage",
Old: []string{"k8s.node.cpu.utilization"},
Kind: KindMetric,
Contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextMetric},
Signals: []telemetrytypes.Signal{telemetrytypes.SignalMetrics},
ApplyToMetrics: nil,
},
{
Current: "k8s.pod.cpu.usage",
Old: []string{"k8s.pod.cpu.utilization"},
Kind: KindMetric,
Contexts: []telemetrytypes.FieldContext{telemetrytypes.FieldContextMetric},
Signals: []telemetrytypes.Signal{telemetrytypes.SignalMetrics},
ApplyToMetrics: nil,
},
}

265
pkg/semconv/semconv.go Normal file
View File

@@ -0,0 +1,265 @@
package semconv
import (
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
//go:generate go run ../../scripts/semconv
// Kind identifies whether a family describes an attribute or a metric name.
type Kind struct {
valuer.String
}
// Family is one logical telemetry field. Old is ordered from the most recent
// predecessor to the oldest one and therefore also defines fallback order.
type Family struct {
Current string
Old []string
Kind Kind
Contexts []telemetrytypes.FieldContext
Signals []telemetrytypes.Signal
ApplyToMetrics []string
ValueMap map[string]string
}
type metricSpelling uint8
const (
metricSpellingDotted metricSpelling = iota
metricSpellingNormalized
)
var (
KindAttribute = Kind{String: valuer.NewString("attribute")}
KindMetric = Kind{String: valuer.NewString("metric")}
)
var memberToFamilies, familyMembers = buildIndexes()
// Enum returns the acceptable values for Kind.
func (Kind) Enum() []any {
return []any{KindAttribute, KindMetric}
}
// Lookup returns the enabled family containing selector.Name for kind. The
// slices and map in the returned value reference immutable generated data and
// must be treated as read-only. Use All when a mutable copy is required.
func Lookup(kind Kind, selector telemetrytypes.FieldKeySelector) (Family, bool) {
idx, ok := lookupIndex(kind, selector)
if !ok {
return Family{}, false
}
return families[idx], true
}
// Members returns the current name first, followed by historical names in
// fallback order. For enabled families the returned precomputed slice is
// read-only. A name outside an enabled family is returned unchanged.
func Members(kind Kind, selector telemetrytypes.FieldKeySelector) []string {
idx, ok := lookupIndex(kind, selector)
if !ok {
return []string{selector.Name}
}
return familyMembers[idx]
}
// AttributeMembers returns the physical attribute spellings that may represent
// selector.Name. Metrics have used both dotted and normalized label layouts;
// resource labels have additionally used a resource_ prefix. Keeping that
// storage detail here prevents metrics readers from maintaining local
// transition tables.
func AttributeMembers(selector telemetrytypes.FieldKeySelector) []string {
if selector.Signal != telemetrytypes.SignalMetrics {
return Members(KindAttribute, selector)
}
lookupSelector := selector
lookupSelector.Name = strings.TrimPrefix(selector.Name, "resource_")
family, style, ok := lookupMetricSpelling(KindAttribute, lookupSelector)
if !ok {
return []string{selector.Name}
}
logicalMembers := familyMembers[family]
result := make([]string, 0, len(logicalMembers)*4)
for _, member := range logicalMembers {
dotted := member
normalized := normalizeMetricSpelling(member)
variants := []string{dotted, normalized}
if style == metricSpellingNormalized {
variants[0], variants[1] = variants[1], variants[0]
}
if selector.FieldContext == telemetrytypes.FieldContextResource ||
selector.FieldContext == telemetrytypes.FieldContextUnspecified ||
strings.HasPrefix(selector.Name, "resource_") {
for _, variant := range variants {
result = appendUniqueString(result, "resource_"+variant)
}
}
for _, variant := range variants {
result = appendUniqueString(result, variant)
}
}
return result
}
// MetricNames returns the current and historical storage names for a metric.
// The input's dotted or normalized style is preserved because both layouts are
// valid metric identities and must not be mixed in one query.
func MetricNames(name string) []string {
selector := telemetrytypes.FieldKeySelector{
Name: name,
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextMetric,
}
family, style, ok := lookupMetricSpelling(KindMetric, selector)
if !ok {
return []string{name}
}
logicalMembers := familyMembers[family]
result := make([]string, 0, len(logicalMembers))
for _, member := range logicalMembers {
if style == metricSpellingNormalized {
member = normalizeMetricSpelling(member)
}
result = appendUniqueString(result, member)
}
return result
}
// CurrentAttribute returns the canonical dotted name for an attribute
// spelling, or selector.Name if no enabled family matches.
func CurrentAttribute(selector telemetrytypes.FieldKeySelector) string {
if selector.Signal != telemetrytypes.SignalMetrics {
return Current(KindAttribute, selector)
}
lookupSelector := selector
lookupSelector.Name = strings.TrimPrefix(selector.Name, "resource_")
family, _, ok := lookupMetricSpelling(KindAttribute, lookupSelector)
if !ok {
return selector.Name
}
return families[family].Current
}
// Current returns the current name for selector.Name, or the input name when
// it does not belong to an enabled family.
func Current(kind Kind, selector telemetrytypes.FieldKeySelector) string {
idx, ok := lookupIndex(kind, selector)
if !ok {
return selector.Name
}
return families[idx].Current
}
// All returns a defensive copy of every enabled family.
func All() []Family {
result := make([]Family, len(families))
for i := range families {
result[i] = cloneFamily(families[i])
}
return result
}
func buildIndexes() (map[string][]int, [][]string) {
index := make(map[string][]int)
members := make([][]string, len(families))
for i, family := range families {
members[i] = make([]string, 0, len(family.Old)+1)
members[i] = append(members[i], family.Current)
members[i] = append(members[i], family.Old...)
index[family.Current] = append(index[family.Current], i)
for _, old := range family.Old {
index[old] = append(index[old], i)
}
}
return index, members
}
func lookupIndex(kind Kind, selector telemetrytypes.FieldKeySelector) (int, bool) {
for _, idx := range memberToFamilies[selector.Name] {
if matchesSelector(families[idx], kind, selector) {
return idx, true
}
}
return 0, false
}
func lookupMetricSpelling(kind Kind, selector telemetrytypes.FieldKeySelector) (int, metricSpelling, bool) {
if idx, ok := lookupIndex(kind, selector); ok {
return idx, metricSpellingDotted, true
}
for idx, family := range families {
if !matchesSelector(family, kind, selector) {
continue
}
for _, member := range familyMembers[idx] {
if normalizeMetricSpelling(member) == selector.Name {
return idx, metricSpellingNormalized, true
}
}
}
return 0, metricSpellingDotted, false
}
func normalizeMetricSpelling(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
func appendUniqueString(values []string, value string) []string {
if value == "" || slices.Contains(values, value) {
return values
}
return append(values, value)
}
func matchesSelector(family Family, kind Kind, selector telemetrytypes.FieldKeySelector) bool {
if family.Kind != kind {
return false
}
if selector.Signal != telemetrytypes.SignalUnspecified && len(family.Signals) > 0 {
if !slices.Contains(family.Signals, selector.Signal) {
return false
}
}
if selector.FieldContext != telemetrytypes.FieldContextUnspecified && len(family.Contexts) > 0 {
if !slices.Contains(family.Contexts, selector.FieldContext) {
return false
}
}
if selector.Signal == telemetrytypes.SignalMetrics && len(family.ApplyToMetrics) > 0 {
if selector.MetricContext == nil {
return false
}
return slices.Contains(family.ApplyToMetrics, selector.MetricContext.MetricName)
}
return true
}
func cloneFamily(family Family) Family {
valueMap := family.ValueMap
family.Old = slices.Clone(family.Old)
family.Contexts = slices.Clone(family.Contexts)
family.Signals = slices.Clone(family.Signals)
family.ApplyToMetrics = slices.Clone(family.ApplyToMetrics)
if valueMap != nil {
family.ValueMap = make(map[string]string, len(valueMap))
for old, current := range valueMap {
family.ValueMap[old] = current
}
}
return family
}

114
pkg/semconv/semconv_test.go Normal file
View File

@@ -0,0 +1,114 @@
package semconv
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMembersReturnsCurrentBeforeHistoricalName(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
}
assert.Equal(t,
[]string{"deployment.environment.name", "deployment.environment"},
Members(KindAttribute, selector),
"members should use current-first fallback order",
)
}
func TestCurrentReturnsCanonicalName(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
}
assert.Equal(t,
"deployment.environment.name",
Current(KindAttribute, selector),
"historical name should resolve to the current family name",
)
}
func TestMembersReturnsInputWhenKindDoesNotMatch(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "deployment.environment",
Signal: telemetrytypes.SignalTraces,
}
assert.Equal(t,
[]string{"deployment.environment"},
Members(KindMetric, selector),
"an attribute family must not match a metric-name lookup",
)
}
func TestAllReturnsDefensiveCopies(t *testing.T) {
first := All()
require.NotEmpty(t, first, "generated families must not be empty")
first[0].Old[0] = "mutated"
second := All()
assert.NotEqual(t, "mutated", second[0].Old[0], "All must not expose mutable generated data")
}
func TestAttributeMembersIncludesMetricResourceStorageSpellings(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "db.system.name",
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextResource,
}
assert.Equal(t, []string{
"resource_db.system.name", "resource_db_system_name", "db.system.name", "db_system_name",
"resource_db.system", "resource_db_system", "db.system", "db_system",
}, AttributeMembers(selector), "resource metric attributes should cover every historical storage layout")
}
func TestCurrentAttributeResolvesNormalizedMetricResourceSpelling(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "resource_db_system",
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextResource,
}
assert.Equal(t, "db.system.name", CurrentAttribute(selector), "normalized resource spelling should resolve to the dotted current name")
}
func TestAttributeMembersPreservesNormalizedMetricPointStyle(t *testing.T) {
selector := telemetrytypes.FieldKeySelector{
Name: "db_system",
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextAttribute,
}
assert.Equal(t, []string{
"db_system_name", "db.system.name", "db_system", "db.system",
}, AttributeMembers(selector), "normalized point attribute should remain the preferred storage spelling")
}
func TestMetricNamesPreservesDottedStyle(t *testing.T) {
assert.Equal(t,
[]string{"k8s.pod.cpu.usage", "k8s.pod.cpu.utilization"},
MetricNames("k8s.pod.cpu.usage"),
"dotted metric input should produce dotted family names",
)
}
func TestMetricNamesPreservesNormalizedStyle(t *testing.T) {
assert.Equal(t,
[]string{"k8s_pod_cpu_usage", "k8s_pod_cpu_utilization"},
MetricNames("k8s_pod_cpu_utilization"),
"normalized metric input should produce normalized family names",
)
}
func TestMetricNamesReturnsUnknownNameUnchanged(t *testing.T) {
assert.Equal(t, []string{"custom_metric"}, MetricNames("custom_metric"), "unknown metrics should not be expanded")
}

View File

@@ -235,6 +235,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewFillDashboardSpecCollectionsFactory(sqlstore, dashboardStore),
sqlmigration.NewScrubEmailChannelTransportFactory(sqlstore),
sqlmigration.NewAddDashboardTuplesFactory(sqlstore),
sqlmigration.NewMigrateDeploymentEnvironmentQuickFilterFactory(),
)
}

View File

@@ -0,0 +1,127 @@
package sqlmigration
import (
"context"
"encoding/json"
"log/slog"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
const deploymentEnvironmentCurrent = "deployment.environment.name"
type migrateDeploymentEnvironmentQuickFilter struct {
logger *slog.Logger
}
type semconvQuickFilterRow struct {
bun.BaseModel `bun:"table:quick_filter"`
ID string `bun:"id"`
Filter string `bun:"filter"`
}
func NewMigrateDeploymentEnvironmentQuickFilterFactory() factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("migrate_semconv_quick_filter"),
func(_ context.Context, settings factory.ProviderSettings, _ Config) (SQLMigration, error) {
return &migrateDeploymentEnvironmentQuickFilter{logger: settings.Logger}, nil
},
)
}
func (migration *migrateDeploymentEnvironmentQuickFilter) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func deploymentEnvironmentOld() string {
members := semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
Name: deploymentEnvironmentCurrent,
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
})
if len(members) < 2 {
return deploymentEnvironmentCurrent
}
return members[1]
}
func rewriteQuickFilterSemconv(filterJSON, from, to string) (string, bool, error) {
var filters []map[string]any
if err := json.Unmarshal([]byte(filterJSON), &filters); err != nil {
return "", false, err
}
changed := false
for _, filter := range filters {
if key, ok := filter["key"].(string); ok && key == from {
filter["key"] = to
changed = true
}
}
if !changed {
return filterJSON, false, nil
}
rewritten, err := json.Marshal(filters)
if err != nil {
return "", false, err
}
return string(rewritten), true, nil
}
func (migration *migrateDeploymentEnvironmentQuickFilter) migrate(ctx context.Context, db *bun.DB, from, to string) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
rows := make([]*semconvQuickFilterRow, 0)
if err := tx.NewSelect().
Model(&rows).
Where("signal IN (?)", bun.In([]string{"traces", "api_monitoring", "exceptions"})).
Scan(ctx); err != nil {
return err
}
for _, row := range rows {
rewritten, changed, err := rewriteQuickFilterSemconv(row.Filter, from, to)
if err != nil {
// Quick filters are user-editable. One malformed legacy row must not
// prevent the application from starting or block every other org's
// migration.
if migration.logger != nil {
migration.logger.WarnContext(ctx, "skipping quick filter with unreadable filter JSON",
slog.String("quick_filter_id", row.ID), slog.Any("error", err))
}
continue
}
if !changed {
continue
}
if _, err := tx.NewUpdate().
Model((*semconvQuickFilterRow)(nil)).
Set("filter = ?", rewritten).
Set("updated_at = ?", time.Now()).
Where("id = ?", row.ID).
Exec(ctx); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *migrateDeploymentEnvironmentQuickFilter) Up(ctx context.Context, db *bun.DB) error {
return migration.migrate(ctx, db, deploymentEnvironmentOld(), deploymentEnvironmentCurrent)
}
func (migration *migrateDeploymentEnvironmentQuickFilter) Down(ctx context.Context, db *bun.DB) error {
return migration.migrate(ctx, db, deploymentEnvironmentCurrent, deploymentEnvironmentOld())
}

View File

@@ -0,0 +1,38 @@
package sqlmigration
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRewriteQuickFilterSemconv(t *testing.T) {
oldName := deploymentEnvironmentOld()
input := `[{"key":"service.name","dataType":"string","type":"resource"},{"key":"` + oldName + `","dataType":"string","type":"resource","custom":true}]`
rewritten, changed, err := rewriteQuickFilterSemconv(input, oldName, deploymentEnvironmentCurrent)
require.NoError(t, err)
assert.True(t, changed)
var filters []map[string]any
require.NoError(t, json.Unmarshal([]byte(rewritten), &filters))
assert.Equal(t, "service.name", filters[0]["key"])
assert.Equal(t, deploymentEnvironmentCurrent, filters[1]["key"])
assert.Equal(t, true, filters[1]["custom"], "unknown filter properties must be preserved")
restored, changed, err := rewriteQuickFilterSemconv(rewritten, deploymentEnvironmentCurrent, oldName)
require.NoError(t, err)
assert.True(t, changed)
require.NoError(t, json.Unmarshal([]byte(restored), &filters))
assert.Equal(t, oldName, filters[1]["key"])
}
func TestRewriteQuickFilterSemconvNoop(t *testing.T) {
input := `[{"key":"service.name","dataType":"string","type":"resource"}]`
rewritten, changed, err := rewriteQuickFilterSemconv(input, deploymentEnvironmentOld(), deploymentEnvironmentCurrent)
require.NoError(t, err)
assert.False(t, changed)
assert.Equal(t, input, rewritten)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/statementbuilder"
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
@@ -31,6 +32,15 @@ const (
OthersMultiTemporality = `IF(LOWER(temporality) LIKE LOWER('delta'), %s, %s) AS per_series_value`
)
func metricNameValues(name string) []any {
members := semconv.MetricNames(name)
values := make([]any, 0, len(members))
for _, member := range members {
values = append(values, member)
}
return values
}
type StatementBuilder struct {
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
@@ -341,7 +351,7 @@ func (b *StatementBuilder) buildReducedTimeSeriesCTE(
sb.SelectMore(col)
}
sb.Where(
sb.In("metric_name", query.Aggregations[0].MetricName),
sb.In("metric_name", metricNameValues(query.Aggregations[0].MetricName)...),
sb.GTE("unix_milli", start),
sb.LTE("unix_milli", end),
)
@@ -385,7 +395,7 @@ func (b *StatementBuilder) buildReducedSpatialAggFastPath(
sb.From(fmt.Sprintf("%s.%s AS points FINAL", metricstelemetryschema.DBName, metricstelemetryschema.WhichReducedSamplesTableToUse(agg.Type)))
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.reduced_fingerprint = filtered_time_series.fingerprint")
sb.Where(
sb.In("metric_name", agg.MetricName),
sb.In("metric_name", metricNameValues(agg.MetricName)...),
sb.GTE("unix_milli", start),
sb.LT("unix_milli", end),
)
@@ -427,7 +437,7 @@ func (b *StatementBuilder) buildReducedTemporalAggregationCTE(
sb.From(fmt.Sprintf("%s.%s AS points FINAL", metricstelemetryschema.DBName, metricstelemetryschema.WhichReducedSamplesTableToUse(agg.Type)))
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.reduced_fingerprint = filtered_time_series.fingerprint")
sb.Where(
sb.In("metric_name", agg.MetricName),
sb.In("metric_name", metricNameValues(agg.MetricName)...),
sb.GTE("unix_milli", start),
sb.LT("unix_milli", end),
)
@@ -505,7 +515,7 @@ func (b *StatementBuilder) buildTemporalAggDeltaFastPath(
sb.From(fmt.Sprintf("%s.%s AS points", metricstelemetryschema.DBName, samplesTable))
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
sb.Where(
sb.In("metric_name", query.Aggregations[0].MetricName),
sb.In("metric_name", metricNameValues(query.Aggregations[0].MetricName)...),
sb.GTE("unix_milli", start),
sb.LT("unix_milli", end),
)
@@ -560,7 +570,7 @@ func (b *StatementBuilder) buildTimeSeriesCTE(
}
sb.Where(
sb.In("metric_name", query.Aggregations[0].MetricName),
sb.In("metric_name", metricNameValues(query.Aggregations[0].MetricName)...),
sb.GTE("unix_milli", start),
sb.LTE("unix_milli", end),
)
@@ -638,7 +648,7 @@ func (b *StatementBuilder) buildTemporalAggDelta(
sb.From(fmt.Sprintf("%s.%s AS points", metricstelemetryschema.DBName, samplesTable))
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
sb.Where(
sb.In("metric_name", query.Aggregations[0].MetricName),
sb.In("metric_name", metricNameValues(query.Aggregations[0].MetricName)...),
sb.GTE("unix_milli", start),
sb.LT("unix_milli", end),
)
@@ -679,7 +689,7 @@ func (b *StatementBuilder) buildTemporalAggCumulativeOrUnspecified(
baseSb.From(fmt.Sprintf("%s.%s AS points", metricstelemetryschema.DBName, samplesTable))
baseSb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
baseSb.Where(
baseSb.In("metric_name", query.Aggregations[0].MetricName),
baseSb.In("metric_name", metricNameValues(query.Aggregations[0].MetricName)...),
baseSb.GTE("unix_milli", start),
baseSb.LT("unix_milli", end),
)
@@ -770,7 +780,7 @@ func (b *StatementBuilder) buildTemporalAggForMultipleTemporalities(
sb.From(fmt.Sprintf("%s.%s AS points", metricstelemetryschema.DBName, samplesTable))
sb.JoinWithOption(sqlbuilder.InnerJoin, timeSeriesCTE, "points.fingerprint = filtered_time_series.fingerprint")
sb.Where(
sb.In("metric_name", query.Aggregations[0].MetricName),
sb.In("metric_name", metricNameValues(query.Aggregations[0].MetricName)...),
sb.GTE("unix_milli", start),
sb.LT("unix_milli", end),
)

View File

@@ -13,9 +13,21 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes/telemetrytypestest"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMetricNameValuesResolveFamily(t *testing.T) {
assert.Equal(t,
[]any{"k8s.pod.cpu.usage", "k8s.pod.cpu.utilization"},
metricNameValues("k8s.pod.cpu.usage"),
)
assert.Equal(t,
[]any{"container_cpu_usage", "container_cpu_utilization"},
metricNameValues("container_cpu_utilization"),
)
}
func TestStatementBuilder(t *testing.T) {
cases := []struct {
name string

View File

@@ -44,6 +44,73 @@ func keyIndexFilter(key *telemetrytypes.TelemetryFieldKey) any {
return fmt.Sprintf(`%%%s%%`, key.Name)
}
func memberKey(key *telemetrytypes.TelemetryFieldKey, name string) *telemetrytypes.TelemetryFieldKey {
member := *key
member.Name = name
return &member
}
func keyIndexCondition(sb *sqlbuilder.SelectBuilder, column string, key *telemetrytypes.TelemetryFieldKey, members []string) string {
conditions := make([]string, 0, len(members))
for _, member := range members {
conditions = append(conditions, sb.Like(column, keyIndexFilter(memberKey(key, member))))
}
if len(conditions) == 1 {
return conditions[0]
}
return sb.Or(conditions...)
}
func valueIndexCondition(
sb *sqlbuilder.SelectBuilder,
column string,
key *telemetrytypes.TelemetryFieldKey,
members []string,
op qbtypes.FilterOperator,
value any,
caseInsensitive bool,
) string {
conditions := make([]string, 0, len(members))
for _, member := range members {
patterns := valueForIndexFilter(op, memberKey(key, member), value)
switch values := patterns.(type) {
case []string:
for _, pattern := range values {
conditions = append(conditions, sb.Like(column, pattern))
}
default:
if caseInsensitive {
conditions = append(conditions, sb.ILike(column, values))
} else {
conditions = append(conditions, sb.Like(column, values))
}
}
}
if len(conditions) == 1 {
return conditions[0]
}
return sb.Or(conditions...)
}
func memberPresenceCondition(sb *sqlbuilder.SelectBuilder, column string, members []string, exists bool) string {
conditions := make([]string, 0, len(members))
for _, member := range members {
field := fmt.Sprintf("simpleJSONHas(%s, '%s')", column, member)
if exists {
conditions = append(conditions, sb.E(field, true))
} else {
conditions = append(conditions, sb.NE(field, true))
}
}
if exists {
if len(conditions) == 1 {
return conditions[0]
}
return sb.Or(conditions...)
}
return sb.And(conditions...)
}
// SkipResourceFilter is not applicable here: the fingerprint table only stores resource attributes.
func (b *defaultConditionBuilder) ConditionFor(
ctx context.Context,
@@ -115,8 +182,10 @@ func (b *defaultConditionBuilder) conditionForKey(
// as we have not changed the resource column in the resource fingerprint table.
column := columns[0]
keyIdxFilter := sb.Like(column.Name, keyIndexFilter(key))
valueForIndexFilter := valueForIndexFilter(op, key, value)
members := resourceSemconvMembers(key)
isFamily := len(members) > 1
keyIdxFilter := keyIndexCondition(sb, column.Name, key, members)
singleValueIndexFilter := valueForIndexFilter(op, memberKey(key, members[0]), value)
fieldName, err := b.fm.FieldFor(ctx, valuer.UUID{}, startNs, endNs, key)
if err != nil {
@@ -128,12 +197,15 @@ func (b *defaultConditionBuilder) conditionForKey(
return sb.And(
sb.E(fieldName, formattedValue),
keyIdxFilter,
sb.Like(column.Name, valueForIndexFilter),
valueIndexCondition(sb, column.Name, key, members, op, value, false),
), nil
case qbtypes.FilterOperatorNotEqual:
if isFamily {
return sb.NE(fieldName, formattedValue), nil
}
return sb.And(
sb.NE(fieldName, formattedValue),
sb.NotLike(column.Name, valueForIndexFilter),
sb.NotLike(column.Name, singleValueIndexFilter),
), nil
case qbtypes.FilterOperatorGreaterThan:
return sb.And(sb.GT(fieldName, formattedValue), keyIdxFilter), nil
@@ -148,7 +220,7 @@ func (b *defaultConditionBuilder) conditionForKey(
return sb.And(
sb.ILike(fieldName, formattedValue),
keyIdxFilter,
sb.ILike(column.Name, valueForIndexFilter),
valueIndexCondition(sb, column.Name, key, members, op, value, true),
), nil
case qbtypes.FilterOperatorNotLike, qbtypes.FilterOperatorNotILike:
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else
@@ -185,13 +257,11 @@ func (b *defaultConditionBuilder) conditionForKey(
inConditions = append(inConditions, sb.E(fieldName, querybuilder.FormatValueForContains(v)))
}
mainCondition := sb.Or(inConditions...)
valConditions := make([]string, 0, len(values))
if valuesForIndexFilter, ok := valueForIndexFilter.([]string); ok {
for _, v := range valuesForIndexFilter {
valConditions = append(valConditions, sb.Like(column.Name, v))
}
}
mainCondition = sb.And(mainCondition, keyIdxFilter, sb.Or(valConditions...))
mainCondition = sb.And(
mainCondition,
keyIdxFilter,
valueIndexCondition(sb, column.Name, key, members, op, value, false),
)
return mainCondition, nil
case qbtypes.FilterOperatorNotIn:
@@ -204,8 +274,11 @@ func (b *defaultConditionBuilder) conditionForKey(
notInConditions = append(notInConditions, sb.NE(fieldName, querybuilder.FormatValueForContains(v)))
}
mainCondition := sb.And(notInConditions...)
if isFamily {
return mainCondition, nil
}
valConditions := make([]string, 0, len(values))
if valuesForIndexFilter, ok := valueForIndexFilter.([]string); ok {
if valuesForIndexFilter, ok := singleValueIndexFilter.([]string); ok {
for _, v := range valuesForIndexFilter {
valConditions = append(valConditions, sb.NotLike(column.Name, v))
}
@@ -215,13 +288,11 @@ func (b *defaultConditionBuilder) conditionForKey(
case qbtypes.FilterOperatorExists:
return sb.And(
sb.E(fmt.Sprintf("simpleJSONHas(%s, '%s')", column.Name, key.Name), true),
memberPresenceCondition(sb, column.Name, members, true),
keyIdxFilter,
), nil
case qbtypes.FilterOperatorNotExists:
return sb.And(
sb.NE(fmt.Sprintf("simpleJSONHas(%s, '%s')", column.Name, key.Name), true),
), nil
return memberPresenceCondition(sb, column.Name, members, false), nil
case qbtypes.FilterOperatorRegexp:
return sb.And(
@@ -237,7 +308,7 @@ func (b *defaultConditionBuilder) conditionForKey(
return sb.And(
sb.ILike(fieldName, fmt.Sprintf(`%%%s%%`, formattedValue)),
keyIdxFilter,
sb.ILike(column.Name, valueForIndexFilter),
valueIndexCondition(sb, column.Name, key, members, op, value, true),
), nil
case qbtypes.FilterOperatorNotContains:
// no index filter: as cannot apply `not contains x%y` as y can be somewhere else

View File

@@ -198,6 +198,75 @@ func TestConditionBuilder(t *testing.T) {
expected: "match(simpleJSONExtractString(labels, 'k8s.namespace.name'), ?) AND labels LIKE ?",
expectedArgs: []any{"ban.*", "%k8s.namespace.name%"},
},
{
name: "semantic convention family equality uses current-first fallback",
key: &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
SemconvMembers: []string{"deployment.environment.name", "deployment.environment"},
},
op: qbtypes.FilterOperatorEqual,
value: "production",
expected: "COALESCE(NULLIF(simpleJSONExtractString(labels, 'deployment.environment.name'), ''), NULLIF(simpleJSONExtractString(labels, 'deployment.environment'), '')) = ? AND (labels LIKE ? OR labels LIKE ?) AND (labels LIKE ? OR labels LIKE ?)",
expectedArgs: []any{
"production",
"%deployment.environment.name%",
"%deployment.environment%",
`%deployment.environment.name":"production%`,
`%deployment.environment":"production%`,
},
},
{
name: "old semantic convention request uses only current metadata member",
key: &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
SemconvMembers: []string{"deployment.environment.name"},
},
op: qbtypes.FilterOperatorEqual,
value: "production",
expected: "simpleJSONExtractString(labels, 'deployment.environment.name') = ? AND labels LIKE ? AND labels LIKE ?",
expectedArgs: []any{"production", "%deployment.environment.name%", `%deployment.environment.name":"production%`},
},
{
name: "semantic convention family negative filter does not reject fallback rows",
key: &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
SemconvMembers: []string{"deployment.environment.name", "deployment.environment"},
},
op: qbtypes.FilterOperatorNotEqual,
value: "staging",
expected: "COALESCE(NULLIF(simpleJSONExtractString(labels, 'deployment.environment.name'), ''), NULLIF(simpleJSONExtractString(labels, 'deployment.environment'), '')) <> ?",
expectedArgs: []any{"staging"},
},
{
name: "semantic convention family exists checks every member",
key: &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
SemconvMembers: []string{"deployment.environment.name", "deployment.environment"},
},
op: qbtypes.FilterOperatorExists,
expected: "(simpleJSONHas(labels, 'deployment.environment.name') = ? OR simpleJSONHas(labels, 'deployment.environment') = ?) AND (labels LIKE ? OR labels LIKE ?)",
expectedArgs: []any{true, true, "%deployment.environment.name%", "%deployment.environment%"},
},
{
name: "semantic convention family not exists checks every member",
key: &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
SemconvMembers: []string{"deployment.environment.name", "deployment.environment"},
},
op: qbtypes.FilterOperatorNotExists,
expected: "(simpleJSONHas(labels, 'deployment.environment.name') <> ? AND simpleJSONHas(labels, 'deployment.environment') <> ?)",
expectedArgs: []any{true, true},
},
}
fm := NewFieldMapper()

View File

@@ -3,8 +3,10 @@ package resourcefilter
import (
"context"
"fmt"
"strings"
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
"github.com/SigNoz/signoz/pkg/semconv"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -32,6 +34,20 @@ func NewFieldMapper() *defaultFieldMapper {
return &defaultFieldMapper{}
}
func resourceSemconvMembers(key *telemetrytypes.TelemetryFieldKey) []string {
if key.FieldContext != telemetrytypes.FieldContextResource {
return []string{key.Name}
}
if len(key.SemconvMembers) > 0 {
return key.SemconvMembers
}
return semconv.Members(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
Name: key.Name,
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
})
}
func (m *defaultFieldMapper) getColumn(
_ context.Context,
_, _ uint64,
@@ -66,7 +82,15 @@ func (m *defaultFieldMapper) FieldFor(
return "", err
}
if key.FieldContext == telemetrytypes.FieldContextResource {
return fmt.Sprintf("simpleJSONExtractString(%s, '%s')", columns[0].Name, key.Name), nil
members := resourceSemconvMembers(key)
if len(members) > 1 {
values := make([]string, 0, len(members))
for _, member := range members {
values = append(values, fmt.Sprintf("NULLIF(simpleJSONExtractString(%s, '%s'), '')", columns[0].Name, member))
}
return "COALESCE(" + strings.Join(values, ", ") + ")", nil
}
return fmt.Sprintf("simpleJSONExtractString(%s, '%s')", columns[0].Name, members[0]), nil
}
return columns[0].Name, nil
}

View File

@@ -1,35 +0,0 @@
package telemetrymetadata
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
type BackwardCompatibleKeyMap map[string]string
var (
TracesBackwardCompatKeys = BackwardCompatibleKeyMap{
"net.peer.name": "server.address",
"server.address": "net.peer.name",
"http.url": "url.full",
"url.full": "http.url",
}
// LogsBackwardCompatKeys contains bidirectional mappings for logs.
// Currently empty, can be extended in the future.
LogsBackwardCompatKeys = BackwardCompatibleKeyMap{}
// MetricsBackwardCompatKeys contains bidirectional mappings for metrics.
// Currently empty, can be extended in the future.
MetricsBackwardCompatKeys = BackwardCompatibleKeyMap{}
)
func GetBackwardCompatKeysForSignal(signal telemetrytypes.Signal) BackwardCompatibleKeyMap {
switch signal {
case telemetrytypes.SignalTraces:
return TracesBackwardCompatKeys
case telemetrytypes.SignalLogs:
return LogsBackwardCompatKeys
case telemetrytypes.SignalMetrics:
return MetricsBackwardCompatKeys
default:
return BackwardCompatibleKeyMap{}
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/telemetryschema/audittelemetryschema"
"github.com/SigNoz/signoz/pkg/telemetryschema/logstelemetryschema"
"github.com/SigNoz/signoz/pkg/telemetryschema/metertelemetryschema"
@@ -151,6 +153,129 @@ func (t *telemetryMetaStore) tracesTblStatementToFieldKeys(ctx context.Context)
return materialisedKeys, nil
}
func attributeSemconvMembers(name string, signal telemetrytypes.Signal, fieldContext telemetrytypes.FieldContext) []string {
return semconv.AttributeMembers(telemetrytypes.FieldKeySelector{
Name: name,
Signal: signal,
FieldContext: fieldContext,
})
}
func traceSemconvMembers(name string, fieldContext telemetrytypes.FieldContext) []string {
return attributeSemconvMembers(name, telemetrytypes.SignalTraces, fieldContext)
}
func semconvDuplicateFactor(signal telemetrytypes.Signal) int {
factor := 1
for _, family := range semconv.All() {
if family.Kind != semconv.KindAttribute {
continue
}
if _, ok := semconv.Lookup(semconv.KindAttribute, telemetrytypes.FieldKeySelector{
Name: family.Current,
Signal: signal,
}); ok {
factor = max(factor, len(attributeSemconvMembers(family.Current, signal, telemetrytypes.FieldContextUnspecified)))
}
}
return factor
}
func traceSemconvDuplicateFactor() int {
return semconvDuplicateFactor(telemetrytypes.SignalTraces)
}
func inStrings(sb *sqlbuilder.SelectBuilder, column string, values []string) string {
args := make([]any, 0, len(values))
for _, value := range values {
args = append(args, value)
}
return sb.In(column, args...)
}
// canonicalizeTraceSemconvKeys presents one current-name key for each family.
// If metadata contains both spellings, metadata attached to the current name
// wins; otherwise the old entry is copied under the current response name.
func canonicalizeTraceSemconvKeys(keys []*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
return canonicalizeSemconvKeys(keys, telemetrytypes.SignalTraces)
}
// canonicalizeSemconvKeys presents one current-name key for each enabled
// attribute family and records only the physical spellings actually present in
// metadata. That lets field mappers retain their optimized single-key SQL for
// homogeneous ranges while mixed ranges coalesce current-first.
func canonicalizeSemconvKeys(keys []*telemetrytypes.TelemetryFieldKey, signal telemetrytypes.Signal) []*telemetrytypes.TelemetryFieldKey {
result := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))
indexByIdentity := make(map[string]int)
currentSourceByIdentity := make(map[string]bool)
for _, key := range keys {
if key.Signal != signal {
result = append(result, key)
continue
}
selector := telemetrytypes.FieldKeySelector{
Name: key.Name,
Signal: signal,
FieldContext: key.FieldContext,
}
members := semconv.AttributeMembers(selector)
current := semconv.CurrentAttribute(selector)
if len(members) == 1 && current == key.Name {
result = append(result, key)
continue
}
resolved := *key
resolved.Name = current
resolved.SemconvMembers = []string{key.Name}
identity := resolved.Name + ";" + resolved.Signal.StringValue() + ";" + resolved.FieldContext.StringValue() + ";" + resolved.FieldDataType.StringValue()
physicalName := strings.TrimPrefix(key.Name, "resource_")
fromCurrent := physicalName == current || physicalName == strings.ReplaceAll(current, ".", "_")
if index, found := indexByIdentity[identity]; found {
physicalMembers := result[index].SemconvMembers
if fromCurrent && !currentSourceByIdentity[identity] {
result[index] = &resolved
currentSourceByIdentity[identity] = true
}
for _, member := range physicalMembers {
if !slices.Contains(result[index].SemconvMembers, member) {
result[index].SemconvMembers = append(result[index].SemconvMembers, member)
}
}
if !slices.Contains(result[index].SemconvMembers, key.Name) {
result[index].SemconvMembers = append(result[index].SemconvMembers, key.Name)
}
continue
}
indexByIdentity[identity] = len(result)
currentSourceByIdentity[identity] = fromCurrent
result = append(result, &resolved)
}
for _, key := range result {
if len(key.SemconvMembers) < 2 {
continue
}
present := make(map[string]bool, len(key.SemconvMembers))
for _, member := range key.SemconvMembers {
present[member] = true
}
ordered := make([]string, 0, len(key.SemconvMembers))
for _, member := range attributeSemconvMembers(key.Name, signal, key.FieldContext) {
if present[member] {
ordered = append(ordered, member)
}
}
key.SemconvMembers = ordered
}
return result
}
// getTracesKeys returns the keys from the spans that match the field selection criteria.
func (t *telemetryMetaStore) getTracesKeys(ctx context.Context, fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
@@ -202,10 +327,23 @@ func (t *telemetryMetaStore) getTracesKeys(ctx context.Context, fieldKeySelector
// key part of the selector
fieldKeyConds := []string{}
members := traceSemconvMembers(fieldKeySelector.Name, fieldKeySelector.FieldContext)
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldKeyConds = append(fieldKeyConds, sb.E("tagKey", fieldKeySelector.Name))
if len(members) == 1 {
fieldKeyConds = append(fieldKeyConds, sb.E("tagKey", members[0]))
} else {
memberValues := make([]any, 0, len(members))
for _, member := range members {
memberValues = append(memberValues, member)
}
fieldKeyConds = append(fieldKeyConds, sb.In("tagKey", memberValues...))
}
} else {
fieldKeyConds = append(fieldKeyConds, sb.ILike("tagKey", "%"+escapeForLike(fieldKeySelector.Name)+"%"))
memberConditions := make([]string, 0, len(members))
for _, member := range members {
memberConditions = append(memberConditions, sb.ILike("tagKey", "%"+escapeForLike(member)+"%"))
}
fieldKeyConds = append(fieldKeyConds, sb.Or(memberConditions...))
}
searchTexts = append(searchTexts, fieldKeySelector.Name)
@@ -238,8 +376,10 @@ func (t *telemetryMetaStore) getTracesKeys(ctx context.Context, fieldKeySelector
mainSb.From(mainSb.BuilderAs(sb, "sub_query"))
mainSb.GroupBy("tag_key", "tag_type", "tag_data_type")
mainSb.OrderBy("priority")
// query one extra to check if we hit the limit
mainSb.Limit(limit + 1)
// Family members collapse after the database query. In the worst case each
// logical key occupies one row per family member, so fetch enough physical
// rows to return the requested logical page, plus one to detect truncation.
mainSb.Limit(limit*traceSemconvDuplicateFactor() + 1)
query, args := mainSb.BuildWithFlavor(sqlbuilder.ClickHouse)
@@ -249,14 +389,7 @@ func (t *telemetryMetaStore) getTracesKeys(ctx context.Context, fieldKeySelector
}
defer rows.Close()
keys := []*telemetrytypes.TelemetryFieldKey{}
rowCount := 0
for rows.Next() {
rowCount++
// reached the limit, we know there are more results
if rowCount > limit {
break
}
var name string
var fieldContext telemetrytypes.FieldContext
var fieldDataType telemetrytypes.FieldDataType
@@ -285,8 +418,11 @@ func (t *telemetryMetaStore) getTracesKeys(ctx context.Context, fieldKeySelector
return nil, false, errors.Wrap(rows.Err(), errors.TypeInternal, errors.CodeInternal, ErrFailedToGetTracesKeys.Error())
}
// hit the limit? (only counting DB results)
complete := rowCount <= limit
keys = canonicalizeTraceSemconvKeys(keys)
complete := len(keys) <= limit
if !complete {
keys = keys[:limit]
}
staticKeys := []string{"isRoot", "isEntryPoint"}
staticKeys = append(staticKeys, maps.Keys(tracestelemetryschema.IntrinsicFields)...)
@@ -460,10 +596,19 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, orgID valuer.UUID,
continue
}
fieldKeyConds := []string{}
members := attributeSemconvMembers(sel.Name, telemetrytypes.SignalLogs, fieldContext)
if sel.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldKeyConds = append(fieldKeyConds, sb.E("name", sel.Name))
memberValues := make([]any, 0, len(members))
for _, member := range members {
memberValues = append(memberValues, member)
}
fieldKeyConds = append(fieldKeyConds, sb.In("name", memberValues...))
} else {
fieldKeyConds = append(fieldKeyConds, sb.ILike("name", "%"+escapeForLike(sel.Name)+"%"))
memberConds := make([]string, 0, len(members))
for _, member := range members {
memberConds = append(memberConds, sb.ILike("name", "%"+escapeForLike(member)+"%"))
}
fieldKeyConds = append(fieldKeyConds, sb.Or(memberConds...))
}
if sel.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
fieldKeyConds = append(fieldKeyConds, sb.E("datatype", sel.FieldDataType.TagDataType()))
@@ -556,6 +701,7 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, orgID valuer.UUID,
limit = 1000
}
dbLimit := limit * semconvDuplicateFactor(telemetrytypes.SignalLogs)
mainQuery := fmt.Sprintf(`
SELECT tag_key, tag_type, tag_data_type, max(priority) as priority
FROM (
@@ -564,7 +710,7 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, orgID valuer.UUID,
GROUP BY tag_key, tag_type, tag_data_type
ORDER BY priority
LIMIT %d
`, strings.Join(queries, " UNION ALL "), limit+1)
`, strings.Join(queries, " UNION ALL "), dbLimit+1)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, mainQuery, allArgs...)
if err != nil {
@@ -585,7 +731,7 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, orgID valuer.UUID,
for rows.Next() {
rowCount++
// reached the limit, we know there are more results
if rowCount > limit {
if rowCount > dbLimit {
break
}
@@ -633,7 +779,16 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, orgID valuer.UUID,
}
// hit the limit? (only counting DB results)
complete := rowCount <= limit
complete := rowCount <= dbLimit
keys = canonicalizeSemconvKeys(keys, telemetrytypes.SignalLogs)
if len(keys) > limit {
keys = keys[:limit]
complete = false
}
mapOfKeys = make(map[string]*telemetrytypes.TelemetryFieldKey, len(keys))
for _, key := range keys {
mapOfKeys[key.Name+";"+key.FieldContext.StringValue()+";"+key.FieldDataType.StringValue()] = key
}
staticKeys := []string{}
staticKeys = append(staticKeys, maps.Keys(logstelemetryschema.IntrinsicFields)...)
@@ -941,10 +1096,19 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
conds := []string{}
for _, fieldKeySelector := range fieldKeySelectors {
fieldConds := []string{}
members := attributeSemconvMembers(fieldKeySelector.Name, telemetrytypes.SignalMetrics, fieldKeySelector.FieldContext)
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldConds = append(fieldConds, sb.E("attr_name", fieldKeySelector.Name))
memberValues := make([]any, 0, len(members))
for _, member := range members {
memberValues = append(memberValues, member)
}
fieldConds = append(fieldConds, sb.In("attr_name", memberValues...))
} else {
fieldConds = append(fieldConds, sb.ILike("attr_name", "%"+escapeForLike(fieldKeySelector.Name)+"%"))
memberConds := make([]string, 0, len(members))
for _, member := range members {
memberConds = append(memberConds, sb.ILike("attr_name", "%"+escapeForLike(member)+"%"))
}
fieldConds = append(fieldConds, sb.Or(memberConds...))
}
fieldConds = append(fieldConds, sb.NotLike("attr_name", "\\_\\_%"))
@@ -960,7 +1124,12 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
if fieldKeySelector.MetricContext != nil {
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
metricNames := semconv.MetricNames(fieldKeySelector.MetricContext.MetricName)
metricValues := make([]any, 0, len(metricNames))
for _, metricName := range metricNames {
metricValues = append(metricValues, metricName)
}
fieldConds = append(fieldConds, sb.In("metric_name", metricValues...))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
@@ -982,7 +1151,8 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
mainSb.GroupBy("name", "field_context", "field_data_type")
mainSb.OrderBy("priority")
// query one extra to check if we hit the limit
mainSb.Limit(limit + 1)
dbLimit := limit * semconvDuplicateFactor(telemetrytypes.SignalMetrics)
mainSb.Limit(dbLimit + 1)
query, args := mainSb.BuildWithFlavor(sqlbuilder.ClickHouse)
@@ -997,7 +1167,7 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
for rows.Next() {
rowCount++
// reached the limit, we know there are more results
if rowCount > limit {
if rowCount > dbLimit {
break
}
@@ -1022,7 +1192,12 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
}
// hit the limit?
complete := rowCount <= limit
complete := rowCount <= dbLimit
keys = canonicalizeSemconvKeys(keys, telemetrytypes.SignalMetrics)
if len(keys) > limit {
keys = keys[:limit]
complete = false
}
return keys, complete, nil
}
@@ -1044,16 +1219,30 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
var limit int
for _, fieldKeySelector := range fieldKeySelectors {
fieldConds := []string{}
members := attributeSemconvMembers(fieldKeySelector.Name, telemetrytypes.SignalMetrics, fieldKeySelector.FieldContext)
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldConds = append(fieldConds, sb.E("attr_name", fieldKeySelector.Name))
memberValues := make([]any, 0, len(members))
for _, member := range members {
memberValues = append(memberValues, member)
}
fieldConds = append(fieldConds, sb.In("attr_name", memberValues...))
} else {
fieldConds = append(fieldConds, sb.Like("attr_name", "%"+fieldKeySelector.Name+"%"))
memberConds := make([]string, 0, len(members))
for _, member := range members {
memberConds = append(memberConds, sb.Like("attr_name", "%"+member+"%"))
}
fieldConds = append(fieldConds, sb.Or(memberConds...))
}
fieldConds = append(fieldConds, sb.NotLike("attr_name", "\\_\\_%"))
if fieldKeySelector.MetricContext != nil {
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
metricNames := semconv.MetricNames(fieldKeySelector.MetricContext.MetricName)
metricValues := make([]any, 0, len(metricNames))
for _, metricName := range metricNames {
metricValues = append(metricValues, metricName)
}
fieldConds = append(fieldConds, sb.In("metric_name", metricValues...))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
@@ -1068,7 +1257,8 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
limit = 1000
}
sb.Limit(limit)
dbLimit := limit * semconvDuplicateFactor(telemetrytypes.SignalMetrics)
sb.Limit(dbLimit + 1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
@@ -1082,7 +1272,7 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
for rows.Next() {
rowCount++
// reached the limit, we know there are more results
if rowCount > limit {
if rowCount > dbLimit {
break
}
@@ -1091,9 +1281,14 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
if err != nil {
return nil, false, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, ErrFailedToGetMeterKeys.Error())
}
fieldContext := telemetrytypes.FieldContextAttribute
if strings.HasPrefix(name, "resource_") {
fieldContext = telemetrytypes.FieldContextResource
}
keys = append(keys, &telemetrytypes.TelemetryFieldKey{
Name: name,
Signal: telemetrytypes.SignalMetrics,
Name: name,
Signal: telemetrytypes.SignalMetrics,
FieldContext: fieldContext,
})
}
@@ -1102,46 +1297,17 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
}
// hit the limit?
complete := rowCount <= limit
complete := rowCount <= dbLimit
keys = canonicalizeSemconvKeys(keys, telemetrytypes.SignalMetrics)
if len(keys) > limit {
keys = keys[:limit]
complete = false
}
return keys, complete, nil
}
// applyBackwardCompatibleKeys adds backward compatible key aliases to the map.
func applyBackwardCompatibleKeys(mapOfKeys map[string][]*telemetrytypes.TelemetryFieldKey) {
// Get backward compatible keys for all signals
backwardCompatKeysBySignal := map[telemetrytypes.Signal]BackwardCompatibleKeyMap{
telemetrytypes.SignalTraces: GetBackwardCompatKeysForSignal(telemetrytypes.SignalTraces),
telemetrytypes.SignalLogs: GetBackwardCompatKeysForSignal(telemetrytypes.SignalLogs),
telemetrytypes.SignalMetrics: GetBackwardCompatKeysForSignal(telemetrytypes.SignalMetrics),
}
// Iterate over existing keys and add aliases if they exist in backward compat mapping
for srcKey, srcKeys := range mapOfKeys {
for _, srcKeyEntry := range srcKeys {
backwardCompatKeys := backwardCompatKeysBySignal[srcKeyEntry.Signal]
if backwardCompatKeys == nil {
continue
}
if aliasKey, ok := backwardCompatKeys[srcKey]; ok {
if _, aliasExists := mapOfKeys[aliasKey]; !aliasExists {
aliasKeyEntry := &telemetrytypes.TelemetryFieldKey{
Name: aliasKey,
Signal: srcKeyEntry.Signal,
FieldContext: srcKeyEntry.FieldContext,
FieldDataType: srcKeyEntry.FieldDataType,
}
mapOfKeys[aliasKey] = []*telemetrytypes.TelemetryFieldKey{aliasKeyEntry}
}
// Found the alias for this signal, no need to check other entries
break
}
}
}
}
func enrichWithIntrinsicMetricKeys(keys map[string][]*telemetrytypes.TelemetryFieldKey, selectors []*telemetrytypes.FieldKeySelector) map[string][]*telemetrytypes.TelemetryFieldKey {
if len(selectors) == 0 {
return keys
@@ -1272,7 +1438,6 @@ func (t *telemetryMetaStore) GetKeys(ctx context.Context, orgID valuer.UUID, fie
mapOfKeys[key.Name] = append(mapOfKeys[key.Name], key)
}
applyBackwardCompatibleKeys(mapOfKeys)
mapOfKeys = enrichWithIntrinsicMetricKeys(mapOfKeys, selectors)
if t.fl.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, featuretypes.NewFlaggerEvaluationContext(orgID)) {
mapOfKeys = enrichWithGenAIKeys(mapOfKeys, selectors)
@@ -1353,7 +1518,6 @@ func (t *telemetryMetaStore) GetKeysMulti(ctx context.Context, orgID valuer.UUID
mapOfKeys[key.Name] = append(mapOfKeys[key.Name], key)
}
applyBackwardCompatibleKeys(mapOfKeys)
mapOfKeys = enrichWithIntrinsicMetricKeys(mapOfKeys, fieldKeySelectors)
if t.fl.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, featuretypes.NewFlaggerEvaluationContext(orgID)) {
mapOfKeys = enrichWithGenAIKeys(mapOfKeys, fieldKeySelectors)
@@ -1367,7 +1531,18 @@ func (t *telemetryMetaStore) GetKey(ctx context.Context, orgID valuer.UUID, fiel
if err != nil {
return nil, err
}
return keys[fieldKeySelector.Name], nil
members := semconv.Members(semconv.KindAttribute, *fieldKeySelector)
resolved := make([]*telemetrytypes.TelemetryFieldKey, 0)
seen := make(map[*telemetrytypes.TelemetryFieldKey]bool)
for _, member := range members {
for _, key := range keys[member] {
if !seen[key] {
resolved = append(resolved, key)
seen[key] = true
}
}
}
return resolved, nil
}
func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.UUID, fieldValueSelector *telemetrytypes.FieldValueSelector) ([]string, bool, error) {
@@ -1486,7 +1661,6 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.
if limit == 0 {
limit = 50
}
// query one extra to check if we hit the limit
sb.Limit(limit + 1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
@@ -1542,7 +1716,16 @@ func (t *telemetryMetaStore) getSpanFieldValues(ctx context.Context, fieldValueS
sb := sqlbuilder.Select("DISTINCT string_value, number_value").From(t.tracesDBName + "." + t.tracesFieldsTblName)
if fieldValueSelector.Name != "" {
sb.Where(sb.E("tag_key", fieldValueSelector.Name))
members := traceSemconvMembers(fieldValueSelector.Name, fieldValueSelector.FieldContext)
if len(members) == 1 {
sb.Where(sb.E("tag_key", members[0]))
} else {
memberValues := make([]any, 0, len(members))
for _, member := range members {
memberValues = append(memberValues, member)
}
sb.Where(sb.In("tag_key", memberValues...))
}
}
// now look at the field context
@@ -1632,7 +1815,12 @@ func (t *telemetryMetaStore) getLogFieldValues(ctx context.Context, fieldValueSe
sb := sqlbuilder.Select("DISTINCT string_value, number_value").From(t.logsDBName + "." + t.logsFieldsTblName)
if fieldValueSelector.Name != "" {
sb.Where(sb.E("tag_key", fieldValueSelector.Name))
members := attributeSemconvMembers(fieldValueSelector.Name, telemetrytypes.SignalLogs, fieldValueSelector.FieldContext)
memberValues := make([]any, 0, len(members))
for _, member := range members {
memberValues = append(memberValues, member)
}
sb.Where(sb.In("tag_key", memberValues...))
}
if fieldValueSelector.FieldContext != telemetrytypes.FieldContextUnspecified {
@@ -1830,7 +2018,9 @@ func (t *telemetryMetaStore) getMetricFieldValues(ctx context.Context, orgID val
From(t.metricsDBName + "." + t.metricsFieldsTblName)
if fieldValueSelector.Name != "" {
sb.Where(sb.E("attr_name", fieldValueSelector.Name))
sb.Where(inStrings(sb, "attr_name", attributeSemconvMembers(
fieldValueSelector.Name, telemetrytypes.SignalMetrics, fieldValueSelector.FieldContext,
)))
}
if fieldValueSelector.FieldContext != telemetrytypes.FieldContextUnspecified {
@@ -1842,7 +2032,7 @@ func (t *telemetryMetaStore) getMetricFieldValues(ctx context.Context, orgID val
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
sb.Where(inStrings(sb, "metric_name", semconv.MetricNames(fieldValueSelector.MetricContext.MetricName)))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
@@ -1976,7 +2166,7 @@ func (t *telemetryMetaStore) getIntrinsicMetricFieldValuesForTable(ctx context.C
From(t.metricsDBName + "." + tableName)
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
sb.Where(inStrings(sb, "metric_name", semconv.MetricNames(fieldValueSelector.MetricContext.MetricName)))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
@@ -2039,12 +2229,14 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
From(t.meterDBName + "." + t.meterFieldsTblName)
if fieldValueSelector.Name != "" {
sb.Where(sb.E("attr.1", fieldValueSelector.Name))
sb.Where(inStrings(sb, "attr.1", attributeSemconvMembers(
fieldValueSelector.Name, telemetrytypes.SignalMetrics, fieldValueSelector.FieldContext,
)))
}
sb.Where(sb.NotLike("attr.1", "\\_\\_%"))
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
sb.Where(inStrings(sb, "metric_name", semconv.MetricNames(fieldValueSelector.MetricContext.MetricName)))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
@@ -2063,8 +2255,10 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
if limit == 0 {
limit = 50
}
// query one extra to check if we hit the limit
sb.Limit(limit + 1)
// A value can be present under several physical spellings; over-fetch and
// de-duplicate after scanning so one family cannot consume the result limit.
dbLimit := limit * semconvDuplicateFactor(telemetrytypes.SignalMetrics)
sb.Limit(dbLimit + 1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
@@ -2074,11 +2268,13 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
defer rows.Close()
values := &telemetrytypes.TelemetryFieldValues{}
seen := make(map[string]bool)
rowCount := 0
uniqueCount := 0
for rows.Next() {
rowCount++
// reached the limit, we know there are more results
if rowCount > limit {
if rowCount > dbLimit {
break
}
@@ -2087,12 +2283,16 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
return nil, false, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, ErrFailedToGetMeterValues.Error())
}
if len(attribute) > 1 {
values.StringValues = append(values.StringValues, attribute[1])
if !seen[attribute[1]] && uniqueCount < limit {
values.StringValues = append(values.StringValues, attribute[1])
seen[attribute[1]] = true
uniqueCount++
}
}
}
// hit the limit?
complete := rowCount <= limit
complete := rowCount <= dbLimit && uniqueCount < limit
return values, complete, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -83,3 +84,121 @@ func TestGetFirstSeenFromMetricMetadata(t *testing.T) {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
func TestCanonicalizeTraceSemconvKeys(t *testing.T) {
oldResource := &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
Description: "old resource metadata",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
currentResource := &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment.name",
Description: "current resource metadata",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
oldAttribute := &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
oldLogResource := &telemetrytypes.TelemetryFieldKey{
Name: "deployment.environment",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
}
result := canonicalizeTraceSemconvKeys([]*telemetrytypes.TelemetryFieldKey{
oldResource,
currentResource,
oldAttribute,
oldLogResource,
})
require.Len(t, result, 3)
assert.Equal(t, "deployment.environment.name", result[0].Name)
assert.Equal(t, "current resource metadata", result[0].Description)
assert.Equal(t, []string{"deployment.environment.name", "deployment.environment"}, result[0].SemconvMembers)
assert.Equal(t, "deployment.environment.name", result[1].Name)
assert.Equal(t, telemetrytypes.FieldContextAttribute, result[1].FieldContext)
assert.Equal(t, []string{"deployment.environment"}, result[1].SemconvMembers)
assert.Equal(t, "deployment.environment", result[2].Name, "phase 1 must not rewrite raw log metadata")
}
func TestCanonicalizeLogAndMetricSemconvKeys(t *testing.T) {
logKeys := canonicalizeSemconvKeys([]*telemetrytypes.TelemetryFieldKey{
{
Name: "db.system",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
{
Name: "db.system.name",
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
}, telemetrytypes.SignalLogs)
require.Len(t, logKeys, 1)
assert.Equal(t, "db.system.name", logKeys[0].Name)
assert.Equal(t, []string{"db.system.name", "db.system"}, logKeys[0].SemconvMembers)
metricKeys := canonicalizeSemconvKeys([]*telemetrytypes.TelemetryFieldKey{
{
Name: "resource_db_system",
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
{
Name: "db.system.name",
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
}, telemetrytypes.SignalMetrics)
require.Len(t, metricKeys, 1)
assert.Equal(t, "db.system.name", metricKeys[0].Name)
assert.Equal(t, []string{"db.system.name", "resource_db_system"}, metricKeys[0].SemconvMembers)
}
func TestGetSpanFieldValuesMergesSemconvFamily(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := NewTelemetryMetaStore(
instrumentationtest.New().ToProviderSettings(),
mockTelemetryStore,
flaggertest.New(t),
)
mock.ExpectQuery(`SELECT DISTINCT string_value, number_value FROM signoz_traces\.distributed_tag_attributes_v2 WHERE tag_key IN \(\?, \?\) AND tag_type = \? AND tag_data_type = \? LIMIT \?`).
WithArgs("deployment.environment.name", "deployment.environment", "resource", "string", 51).
WillReturnRows(cmock.NewRows([]cmock.ColumnType{
{Name: "string_value", Type: "String"},
{Name: "number_value", Type: "Float64"},
}, [][]any{
{"production", float64(0)},
{"staging", float64(0)},
{"production", float64(0)},
}))
values, complete, err := metadata.GetAllValues(context.Background(), valuer.UUID{}, &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalTraces,
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Name: "deployment.environment",
},
})
require.NoError(t, err)
assert.True(t, complete)
assert.Equal(t, []string{"production", "staging"}, values.StringValues)
assert.NoError(t, mock.ExpectationsWereMet(), "all expected metadata queries should be executed")
}

View File

@@ -0,0 +1,156 @@
package telemetrymetadata
import (
"context"
"fmt"
"slices"
"strings"
"github.com/huandu/go-sqlbuilder"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type semconvMigrationRow struct {
current string
old string
signal string
service string
resourceSets uint64
lastSeenUnixMilli int64
}
// GetSemconvMigrationReport derives an old-only service report from the
// generated family registry and attributes_metadata. The latter is already a
// deduplicated set of resource/attribute fingerprints, so this audit avoids a
// scan of the raw telemetry tables.
func (t *telemetryMetaStore) GetSemconvMigrationReport(
ctx context.Context,
_ valuer.UUID,
startUnixMilli, endUnixMilli int64,
) (*telemetrytypes.GettableSemconvMigrationReport, error) {
query, args := t.semconvMigrationReportQuery(startUnixMilli, endUnixMilli)
report := &telemetrytypes.GettableSemconvMigrationReport{
StartUnixMilli: startUnixMilli,
EndUnixMilli: endUnixMilli,
Entries: []*telemetrytypes.SemconvMigrationReportEntry{},
}
if query == "" {
return report, nil
}
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, "failed to build semantic-convention migration report")
}
defer rows.Close()
grouped := make(map[string]*telemetrytypes.SemconvMigrationReportEntry)
serviceSets := make(map[string]map[string]struct{})
for rows.Next() {
var row semconvMigrationRow
if err := rows.Scan(
&row.current,
&row.old,
&row.signal,
&row.service,
&row.resourceSets,
&row.lastSeenUnixMilli,
); err != nil {
return nil, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, "failed to scan semantic-convention migration report")
}
identity := row.current + "\x00" + row.old + "\x00" + row.signal
entry, ok := grouped[identity]
if !ok {
entry = &telemetrytypes.SemconvMigrationReportEntry{
Current: row.current,
Old: row.old,
Signal: row.signal,
Services: []string{},
}
grouped[identity] = entry
serviceSets[identity] = make(map[string]struct{})
report.Entries = append(report.Entries, entry)
}
serviceSets[identity][row.service] = struct{}{}
entry.ResourceSets += row.resourceSets
entry.LastSeenUnixMilli = max(entry.LastSeenUnixMilli, row.lastSeenUnixMilli)
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, "failed to read semantic-convention migration report")
}
for identity, entry := range grouped {
for service := range serviceSets[identity] {
entry.Services = append(entry.Services, service)
}
slices.Sort(entry.Services)
}
slices.SortFunc(report.Entries, func(a, b *telemetrytypes.SemconvMigrationReportEntry) int {
return strings.Compare(a.Current+"\x00"+a.Old+"\x00"+a.Signal, b.Current+"\x00"+b.Old+"\x00"+b.Signal)
})
return report, nil
}
func (t *telemetryMetaStore) semconvMigrationReportQuery(startUnixMilli, endUnixMilli int64) (string, []any) {
builders := make([]sqlbuilder.Builder, 0)
for _, family := range semconv.All() {
if family.Kind != semconv.KindAttribute {
continue
}
for _, old := range family.Old {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(
fmt.Sprintf("%s AS current_name", sb.Var(family.Current)),
fmt.Sprintf("%s AS old_name", sb.Var(old)),
"data_source",
"if(empty(resource_attributes['service.name']), '<unknown>', resource_attributes['service.name']) AS service_name",
"uniqExact(tuple(resource_fingerprint, attrs_fingerprint)) AS resource_sets",
"max(unix_milli) AS last_seen_unix_milli",
)
sb.From(t.relatedMetadataDBName + "." + t.relatedMetadataTblName)
sb.Where(sb.GE("unix_milli", startUnixMilli))
sb.Where(sb.LE("unix_milli", endUnixMilli))
if len(family.Signals) > 0 {
signals := make([]any, 0, len(family.Signals))
for _, signal := range family.Signals {
signals = append(signals, signal.StringValue())
}
sb.Where(sb.In("data_source", signals...))
}
oldPresence := semconvMetadataPresenceConditions(sb, old)
currentPresence := semconvMetadataPresenceConditions(sb, family.Current)
sb.Where(sb.Or(oldPresence...))
sb.Where(fmt.Sprintf("NOT (%s)", sb.Or(currentPresence...)))
sb.GroupBy("data_source", "service_name")
builders = append(builders, sb)
}
}
if len(builders) == 0 {
return "", nil
}
union := sqlbuilder.UnionAll(builders...)
return union.BuildWithFlavor(sqlbuilder.ClickHouse)
}
func semconvMetadataPresenceConditions(sb *sqlbuilder.SelectBuilder, name string) []string {
spellings := []string{name, strings.ReplaceAll(name, ".", "_")}
spellings = append(spellings, "resource_"+name, "resource_"+strings.ReplaceAll(name, ".", "_"))
spellings = slices.Compact(spellings)
conditions := make([]string, 0, len(spellings)*2)
for _, spelling := range spellings {
conditions = append(conditions,
fmt.Sprintf("mapContains(resource_attributes, %s)", sb.Var(spelling)),
fmt.Sprintf("mapContains(attributes, %s)", sb.Var(spelling)),
)
}
return conditions
}

View File

@@ -89,10 +89,12 @@ func (v *TelemetryFieldVisitor) VisitColumnDef(expr *parser.ColumnDef) error {
// Create and store the TelemetryFieldKey
field := &telemetrytypes.TelemetryFieldKey{
Name: fieldName,
FieldContext: fieldContext,
FieldDataType: fieldDataType,
Materialized: true,
Name: fieldName,
FieldContext: fieldContext,
FieldDataType: fieldDataType,
Materialized: true,
MaterializedColumnName: columnName,
MaterializedSemconv: strings.Count(defaultExprStr, "['") > 1,
}
v.Fields = append(v.Fields, field)

View File

@@ -5,8 +5,25 @@ import (
"testing"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExtractFieldKeyPreservesHistoricalMaterializedColumnName(t *testing.T) {
statement := `CREATE TABLE signoz_traces.signoz_index_v3
(
attributes_string Map(LowCardinality(String), String),
` + "`attribute_string_db$$system`" + ` LowCardinality(String)
DEFAULT if(mapContains(attributes_string, 'db.system.name'), attributes_string['db.system.name'], attributes_string['db.system'])
) ENGINE = MergeTree ORDER BY tuple()`
keys, err := ExtractFieldKeysFromTblStatement(statement)
require.NoError(t, err, "table statement should parse")
require.Len(t, keys, 1, "table statement should contain one materialized key")
assert.Equal(t, "db.system.name", keys[0].Name)
assert.Equal(t, "attribute_string_db$$system", keys[0].MaterializedColumnName)
assert.True(t, keys[0].MaterializedSemconv)
}
func TestExtractFieldKeysFromTblStatement(t *testing.T) {
var statement = `CREATE TABLE signoz_logs.logs_v2

View File

@@ -453,6 +453,9 @@ func (c *conditionBuilder) ConditionFor(
sb *sqlbuilder.SelectBuilder,
) ([]string, []string, error) {
matches := querybuilder.MatchingFieldKeys(key, fieldKeys)
if options.ExactSemconv {
matches = querybuilder.MatchingFieldKeysExact(key, fieldKeys)
}
skipResourceFilter := options.SkipResourceFilter
// search() resolves its own (optional) scope; handle it before key resolution.
@@ -499,6 +502,9 @@ func (c *conditionBuilder) ConditionFor(
warnings = append(warnings, querybuilder.NewKeyNotFoundWarning(key.Name))
}
}
if options.ExactSemconv {
keys = querybuilder.ExactSemconvKeys(keys)
}
if skipResourceFilter && !synthesized {
filtered := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/semconv"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
@@ -67,6 +68,20 @@ type fieldMapper struct {
fl flagger.Flagger
}
func logSemconvMembers(key *telemetrytypes.TelemetryFieldKey) []string {
if key.FieldContext != telemetrytypes.FieldContextResource && key.FieldContext != telemetrytypes.FieldContextAttribute {
return []string{key.Name}
}
if len(key.SemconvMembers) > 0 {
return key.SemconvMembers
}
return semconv.AttributeMembers(telemetrytypes.FieldKeySelector{
Name: key.Name,
Signal: telemetrytypes.SignalLogs,
FieldContext: key.FieldContext,
})
}
func NewFieldMapper(fl flagger.Flagger) qbtypes.FieldMapper {
return &fieldMapper{fl: fl}
}
@@ -141,8 +156,20 @@ func (m *fieldMapper) FieldFor(ctx context.Context, orgID valuer.UUID, tsStart,
case schema.ColumnTypeEnumJSON:
switch key.FieldContext {
case telemetrytypes.FieldContextResource:
exprs = append(exprs, fmt.Sprintf("%s.`%s`::String", columnName, key.Name))
existExpr = append(existExpr, fmt.Sprintf("%s.`%s` IS NOT NULL", columnName, key.Name))
members := logSemconvMembers(key)
if len(members) > 1 {
values := make([]string, 0, len(members))
guards := make([]string, 0, len(members))
for _, member := range members {
values = append(values, fmt.Sprintf("NULLIF(%s.`%s`::String, '')", columnName, member))
guards = append(guards, fmt.Sprintf("%s.`%s` IS NOT NULL", columnName, member))
}
exprs = append(exprs, "COALESCE("+strings.Join(values, ", ")+")")
existExpr = append(existExpr, "("+strings.Join(guards, " OR ")+")")
} else {
exprs = append(exprs, fmt.Sprintf("%s.`%s`::String", columnName, members[0]))
existExpr = append(existExpr, fmt.Sprintf("%s.`%s` IS NOT NULL", columnName, members[0]))
}
case telemetrytypes.FieldContextBody:
if key.Name == messageSubField {
exprs = append(exprs, messageSubColumn)
@@ -181,13 +208,32 @@ func (m *fieldMapper) FieldFor(ctx context.Context, orgID valuer.UUID, tsStart,
switch valueType := column.Type.(schema.MapColumnType).ValueType; valueType.GetType() {
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumBool, schema.ColumnTypeEnumFloat64:
// a key could have been materialized, if so return the materialized column name
if key.Materialized {
members := logSemconvMembers(key)
if key.Materialized && (len(members) == 1 || key.MaterializedSemconv) {
exprs = append(exprs, telemetrytypes.FieldKeyToMaterializedColumnName(key))
existExpr = append(existExpr, telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key))
} else if len(members) > 1 {
guards := make([]string, 0, len(members))
for _, member := range members {
guards = append(guards, fmt.Sprintf("mapContains(%s, '%s')", columnName, member))
}
if valueType.GetType() == schema.ColumnTypeEnumString {
values := make([]string, 0, len(members))
for _, member := range members {
values = append(values, fmt.Sprintf("NULLIF(%s['%s'], '')", columnName, member))
}
exprs = append(exprs, "COALESCE("+strings.Join(values, ", ")+")")
} else {
branches := make([]string, 0, len(members)*2+1)
for i, member := range members {
branches = append(branches, guards[i], fmt.Sprintf("%s['%s']", columnName, member))
}
exprs = append(exprs, "multiIf("+strings.Join(branches, ", ")+", NULL)")
}
existExpr = append(existExpr, "("+strings.Join(guards, " OR ")+")")
} else {
exprs = append(exprs, fmt.Sprintf("%s['%s']", columnName, key.Name))
existExpr = append(existExpr, fmt.Sprintf("mapContains(%s, '%s')", columnName, key.Name))
exprs = append(exprs, fmt.Sprintf("%s['%s']", columnName, members[0]))
existExpr = append(existExpr, fmt.Sprintf("mapContains(%s, '%s')", columnName, members[0]))
}
default:
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "exists operator is not supported for map column type %s", valueType)

Some files were not shown because too many files have changed in this diff Show More