Compare commits

..

38 Commits

Author SHA1 Message Date
Nikhil Mantri
330336a098 Merge branch 'main' into feat/google_chat_alert_integration_tests-refactored 2026-10-06 17:48:20 +05:30
Nikhil Mantri
32350e61e5 feat(ruler): add saved views CRUD APIs for the alerts list page (#12814)
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
#### Description

- Adds saved views for the alerts list page: a view stores the v3 list
params (`query` DSL string, `states`, `sort`, `order`) and the FE
replays them, mirroring dashboard saved views.
- New CRUD APIs: `GET/POST /api/v2/rule_views`, `PUT/DELETE
/api/v2/rule_views/{id}`. Open to all roles including viewer, like
dashboard views; shared org-wide.
- New `rule_view` table (migration 127), same shape as `dashboard_view`
plus an explicit index on `org_id` (neither Postgres nor SQLite
auto-indexes an FK's referencing column).
- Validation on save: name (required, trimmed, max 64), `version` must
be `v1`, query length-capped at 1024 (not compiled, a work-in-progress
query is saveable), `states`/`sort`/`order` enum-checked; zero
sort/order normalize to the list defaults (`updated_at`/`desc`); unknown
body fields rejected.
- Covered by unit tests and a new integration suite
(`alerts/07_rule_views.py`: rejection table, 404s, full lifecycle);
OpenAPI spec regenerated.
- discussion: SigNoz/pulse-pod#304.

#### Issues closed by this PR

Closes SigNoz/pulse-pod#227

#### Additional Information

- Based on #12780 (alerts listing page revamp), which this PR's branch
is cut from; backend only, FE ships separately.

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-10-06 09:35:15 +00:00
Gaurav Tewari
2bd0153a51 chore: replace banner in llm with callout (#13037)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

Prev. we had a banner with this hardcoded values. now we have replaced
it. the callout component.


#### Issues closed by this PR

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings


before - 
<img width="1517" height="325" alt="image"
src="https://github.com/user-attachments/assets/257639f0-0e97-4606-b9c9-55ee8cabe53f"
/>

Now - 
<img width="1515" height="339" alt="image"
src="https://github.com/user-attachments/assets/b02a9cae-3fc0-4980-90c9-2bde1c9b2c98"
/>

#### Additional Information

<!--Please delete paragraphs that you did not use before submitting.-->

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-10-06 05:57:19 +00:00
dependabot[bot]
23776bbd26 chore(deps): bump urllib3 from 2.7.0 to 2.8.0 in /tests (#13032)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.7.0 to 2.8.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/releases">urllib3's
releases</a>.</em></p>
<blockquote>
<h2>2.8.0</h2>
<h2>🚀 urllib3 is fundraising for HTTP/2 support</h2>
<p><a
href="https://sethmlarson.dev/urllib3-is-fundraising-for-http2-support">urllib3
is raising ~$40,000 USD</a> to release HTTP/2 support and ensure
long-term sustainable maintenance of the project. If your company or
organization uses Python and would benefit from HTTP/2 support in
Requests, pip, cloud SDKs, and thousands of other projects <a
href="https://opencollective.com/urllib3">please consider contributing
financially</a> to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.</p>
<p>Thank you for your support.</p>
<h2>Security</h2>
<p>Fixed the following security issues:</p>
<ul>
<li>The TLS configuration for HTTPS proxies could be ignored or
overridden. (High severity, GHSA-8988-9cw3-xx77)</li>
<li><code>HTTPResponse.stream()</code> and <code>read_chunked()</code>
could buffer a chunk-size line of unbounded length in memory. (High
severity, GHSA-vxq7-64xx-v4gw)</li>
<li>Chunked Deflate streaming could enter an infinite loop. (Medium
severity, GHSA-gh4c-6fx4-qh6g)</li>
</ul>
<blockquote>
<p>[!IMPORTANT]
urllib3 2.8.0 fixes HTTPS proxy TLS configuration being ignored or
overridden by destination settings. Configurations relying on that
behavior may require changes.</p>
<p>Configure proxy CA certificates and client certificates in
<code>proxy_ssl_context</code>, and proxy identity checks with
<code>proxy_assert_hostname</code> or
<code>proxy_assert_fingerprint</code>. Destination client certificates
and identity overrides no longer apply to HTTPS forwarding proxy
connections.</p>
</blockquote>
<blockquote>
<p>[!NOTE]
CVE IDs had not yet been assigned to these advisories at the time of
release due to a backlog at GitHub's CNA.</p>
</blockquote>
<h2>Deprecations &amp; Removals</h2>
<ul>
<li>Deprecated using an empty collection as the <code>Retry</code>
option <code>allowed_methods</code> to retry any verb. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5044">#5044</a>)</li>
</ul>
<h2>Features</h2>
<ul>
<li>Added <code>Url.auth_decoded</code> and
<code>Url.auth_decoded_joined</code> convenience properties to the
result of <code>parse_url()</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/4945">#4945</a>)</li>
<li>Added <code>basic_auth_encoding</code> and
<code>proxy_basic_auth_encoding</code> parameters to
<code>urllib3.util.make_headers()</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5092">#5092</a>)</li>
</ul>
<h2>Bugfixes</h2>
<ul>
<li>
<p>Fixed response header handling to replace obsolete folded header
lines (<code>obs-fold</code>) with spaces in accordance with RFC 9112,
preventing raw CRLF sequences from appearing in header values such as
<code>Set-Cookie</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/1362">#1362</a>)</p>
</li>
<li>
<p>Fixed usage of <code>proxy_ssl_context</code> with
<code>ProxyManager</code> when
<code>use_forwarding_for_https=True</code>. Passing
<code>ssl_context</code> instead of <code>proxy_ssl_context</code> for
HTTPS proxies in this configuration now emits a
<code>FutureWarning</code> and will raise an error in v3.0. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/2577">#2577</a>)</p>
</li>
<li>
<p>Changed behavior of the default <code>ConnectionPool.pool</code>
initialization. <code>LifoQueue</code> is now resolved from the
<code>queue</code> module after the <code>ConnectionPool</code> is
instantiated instead of using the default cached <code>QueueCls</code>
class property. This is done because sometimes the
<code>queue.LifoQueue</code> is monkey-patched late in the program, such
as by gevent. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3289">#3289</a>)</p>
</li>
<li>
<p>Raised <code>UnrewindableBodyError</code> instead of
<code>ValueError</code> when retrying a request whose body had
<code>tell()</code> but not <code>seek()</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3779">#3779</a>)</p>
</li>
<li>
<p>Decoded percent-encoded SOCKS proxy credentials before authenticating
with the proxy server. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/3785">#3785</a>)</p>
</li>
<li>
<p>Fixed <code>HTTPResponse.drain_conn()</code> to discard unread
response data in 64 KiB chunks (same as the default <code>amt</code>
when doing <code>HTTPResponse.stream(...)</code>). (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5019">#5019</a>)</p>
</li>
<li>
<p>Fixed <code>is_ipaddress()</code> to detect non-standard IPv4 forms
accepted by <code>socket.connect</code>, such as hex
(<code>0x7f000001</code>), octal (<code>0177.0.0.1</code>), and decimal
integers (<code>2130706433</code>), ensuring SSL certificate
verification uses the correct mode for these addresses. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5029">#5029</a>)</p>
</li>
<li>
<p>Fixed <code>HTTPConnectionPool.urlopen</code> raising a misleading
<code>FullPoolError</code> instead of <code>ValueError</code> when
called with an invalid <code>timeout</code> argument on a pool created
with <code>block=True</code>. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5059">#5059</a>)</p>
</li>
<li>
<p>Fixed port-zero handling to preserve explicit <code>:0</code> values
instead of substituting the default ports 80 or 443 in URL parsing, pool
selection, proxy configuration, <code>connection_from_url()</code>, and
HTTP/2 request authority. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5071">#5071</a>,
<a
href="https://redirect.github.com/urllib3/urllib3/issues/5101">#5101</a>)</p>
</li>
<li>
<p>Fixed a bug where <code>PoolManager</code> passed the
<code>assert_hostname</code> and <code>assert_fingerprint</code>
parameters to HTTP connection pools. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5077">#5077</a>)</p>
</li>
<li>
<p>Fixed <code>HTTPConnectionPool.urlopen()</code> and HTTP proxy
forwarding to strip URL fragments from absolute request targets before
sending requests. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5079">#5079</a>)</p>
</li>
<li>
<p>Added safeguards to the proxy tunneling code to prevent potential
security issues when handling invalid characters in the proxy host and
HTTP headers. This change affects users of Python 3.10, Python 3.11, and
Python 3.12 when the standard library does not contain the fix; those on
newer Python versions should upgrade to 3.13.14+ or 3.14.5+ to get the
same security fixes. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5091">#5091</a>)</p>
</li>
<li>
<p>Fixed <code>HTTPSConnection.connect()</code> overriding
<code>ProxyConfig.ssl_context</code>'s certificate policy and proxy
identity checks with the target connection's TLS settings when
forwarding through an HTTPS proxy.</p>
<p><code>HTTPSConnection</code> no longer applies target SNI,
assertions, or client credentials to forwarding proxy handshakes and
continues to use its <code>ssl_context</code> as a fallback when an
HTTPS proxy forwards an HTTP target. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5093">#5093</a>)</p>
</li>
<li>
<p>Fixed URL parsing to more strictly enforce RFC 3986 host syntax,
rejecting invalid host input such as raw spaces and control characters,
malformed percent-encodings, and percent-encoded control characters in
HTTP(S) hosts and IPv6 zone identifiers, including proxy CONNECT tunnel
targets. Host normalization now also follows RFC 3986 normalization
rules for percent-encoded octets by decoding percent-encoded unreserved
characters and uppercasing the hexadecimal digits of retained
percent-encoded octets. (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5095">#5095</a>)</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/urllib3/urllib3/blob/main/CHANGES.rst">urllib3's
changelog</a>.</em></p>
<blockquote>
<h1>2.8.0 (2026-09-15)</h1>
<h2>Security</h2>
<p>Fixed the following security issues:</p>
<ul>
<li>The TLS configuration for HTTPS proxies could be ignored or
overridden.
(High severity, <code>GHSA-8988-9cw3-xx77
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-8988-9cw3-xx77&gt;</code>__)</li>
<li><code>HTTPResponse.stream()</code> and <code>read_chunked()</code>
could buffer a chunk-size
line of unbounded length in memory. (High severity,
<code>GHSA-vxq7-64xx-v4gw
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-vxq7-64xx-v4gw&gt;</code>__)</li>
<li>Chunked Deflate streaming could enter an infinite loop. (Medium
severity,
<code>GHSA-gh4c-6fx4-qh6g
&lt;https://github.com/urllib3/urllib3/security/advisories/GHSA-gh4c-6fx4-qh6g&gt;</code>__)</li>
</ul>
<p>.. caution::</p>
<pre><code>urllib3 2.8.0 fixes HTTPS proxy TLS configuration being
ignored or
overridden by destination settings. Configurations relying on that
behavior may require changes.
<p>Configure proxy CA certificates and client certificates in
<code>proxy_ssl_context</code>, and proxy identity checks with
<code>proxy_assert_hostname</code> or
<code>proxy_assert_fingerprint</code>.
Destination client certificates and identity overrides no longer
apply to HTTPS forwarding proxy connections.
</code></pre></p>
<h2>Deprecations &amp; Removals</h2>
<ul>
<li>Deprecated using an empty collection as the <code>Retry</code>
option
<code>allowed_methods</code> to retry any verb.
(<code>[#5044](https://github.com/urllib3/urllib3/issues/5044)
&lt;https://github.com/urllib3/urllib3/issues/5044&gt;</code>__)</li>
</ul>
<h2>Features</h2>
<ul>
<li>Added <code>Url.auth_decoded</code> and
<code>Url.auth_decoded_joined</code> convenience
properties to the result of <code>parse_url()</code>.
(<code>[#4945](https://github.com/urllib3/urllib3/issues/4945)
&lt;https://github.com/urllib3/urllib3/issues/4945&gt;</code>__)</li>
<li>Added <code>basic_auth_encoding</code> and
<code>proxy_basic_auth_encoding</code> parameters to
<code>urllib3.util.make_headers()</code>.
(<code>[#5092](https://github.com/urllib3/urllib3/issues/5092)
&lt;https://github.com/urllib3/urllib3/issues/5092&gt;</code>__)</li>
</ul>
<h2>Bugfixes</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b1d30ab61f"><code>b1d30ab</code></a>
Release 2.8.0</li>
<li><a
href="9016d7e8af"><code>9016d7e</code></a>
Skip <code>test_read_chunked_with_trailing_data_does_not_hang</code> for
brotlicffi (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5258">#5258</a>)</li>
<li><a
href="9101f581a8"><code>9101f58</code></a>
Fix <code>nox -s docs</code> warning (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5256">#5256</a>)</li>
<li><a
href="cd770b059b"><code>cd770b0</code></a>
Merge commit from fork</li>
<li><a
href="ea2ad7b21a"><code>ea2ad7b</code></a>
Merge commit from fork</li>
<li><a
href="0716e31534"><code>0716e31</code></a>
Fix loading unencrypted client keys with a password in pyOpenSSL (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5255">#5255</a>)</li>
<li><a
href="43c68c8b43"><code>43c68c8</code></a>
Test pickling of <code>InvalidChunkLength</code> (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5247">#5247</a>)</li>
<li><a
href="308b279b3f"><code>308b279</code></a>
Share security policy between GitHub and Read the Docs (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5253">#5253</a>)</li>
<li><a
href="53fa0731b2"><code>53fa073</code></a>
Add policy on duplicate pull requests (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5252">#5252</a>)</li>
<li><a
href="5f2a6a843d"><code>5f2a6a8</code></a>
Assert on the ALPN extension in test_tunnel_sets_http_11_alpn (<a
href="https://redirect.github.com/urllib3/urllib3/issues/5232">#5232</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/urllib3/urllib3/compare/2.7.0...2.8.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 04:07:41 +00:00
Srikanth Chekuri
f941d1ea11 chore(deps): bump grpc to v1.83.2 and opentelemetry-go to v1.45.0 (#13047)
#### Description

- Bump `google.golang.org/grpc` to v1.83.2 and the opentelemetry-go core
and trace exporter modules to v1.45.0. This fixes the open Dependabot
alerts for these modules.
- Take the resource schema URL from `sdkresource.Default()`. From
v1.44.0, the SDK uses semconv 1.41.0. With the pinned 1.40.0 URL, the
resource merge fails and the server does not start.
- Keep the otel log modules at their current versions. Their v0.21.0 API
breaks `otelzap` v0.13.0 and the log exporters that
`signoz-otel-collector` v0.144.6 uses.
- Replaces #12828, #12900, #12902, #12903 and #12904.
2026-10-06 03:23:37 +00:00
dependabot[bot]
185677634c chore(deps): bump google.golang.org/grpc from 1.83.1 to 1.83.2 in /scripts/promqltestcorpus (#12804)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from
1.83.1 to 1.83.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's
releases</a>.</em></p>
<blockquote>
<h2>Release 1.83.2</h2>
<h1>Security</h1>
<ul>
<li>server: Reject requests missing both <code>:authority</code> and
<code>Host</code> headers with HTTP 400 and status
<code>Internal</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/pull/9365">grpc/grpc-go#9365</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/winklemad"><code>@​winklemad</code></a></li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="030ee8becb"><code>030ee8b</code></a>
Update version to 1.83.2 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9375">#9375</a>)</li>
<li><a
href="8668b69c16"><code>8668b69</code></a>
cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/9365">#9365</a> to
v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9366">#9366</a>)</li>
<li><a
href="a3e952d2b7"><code>a3e952d</code></a>
cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/9346">#9346</a> to
v1.83.x and update x/net dependency (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9369">#9369</a>)</li>
<li><a
href="58f8fd9a00"><code>58f8fd9</code></a>
Change version to 1.83.2-dev (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9337">#9337</a>)</li>
<li>See full diff in <a
href="https://github.com/grpc/grpc-go/compare/v1.83.1...v1.83.2">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-05 13:04:53 +00:00
Praneeth Lingam
79cbc8db6a Merge branch 'main' of github.com:SigNoz/signoz into feat/google_chat_alert_integration_tests-refactored 2026-10-01 19:26:43 +05:30
Praneeth Lingam
d869cae9b4 Merge branch 'main' of github.com:SigNoz/signoz into feat/google_chat_alert_integration_tests-refactored 2026-10-01 16:39:21 +05:30
Praneeth Lingam
717b4aa164 test(alert-channel-integrations): fixed file numbering clashes 2026-10-01 16:29:13 +05:30
Praneeth Lingam
62cd8d8517 Merge branch 'main' of github.com:SigNoz/signoz into feat/google_chat_alert_integration_tests-refactored 2026-10-01 14:56:23 +05:30
Praneeth Lingam
908587abbc test(alert-channel-integrations): removed tls for few channels; added tmpfs 2026-10-01 14:56:04 +05:30
Praneeth Lingam
f5e9ae0a7f Merge branch 'main' of github.com:SigNoz/signoz into feat/google_chat_alert_integration_tests-refactored 2026-09-30 17:13:33 +05:30
Praneeth Lingam
e5a246468a Merge branch 'main' of github.com:SigNoz/signoz into feat/google_chat_alert_integration_tests-refactored 2026-09-29 14:01:17 +05:30
Praneeth Lingam
bb35299162 Merge branch 'main' of github.com:SigNoz/signoz into feat/google_chat_alert_integration_tests-refactored 2026-09-25 15:34:00 +05:30
Praneeth Lingam
3e2b242760 test(alerts): moved alerts related tests to alerts folder 2026-09-25 15:18:53 +05:30
Praneeth Lingam
468c1dfad3 chore(tests): using webhook channel in list rules test 2026-09-25 12:35:48 +05:30
Praneeth Lingam
91c14e92db chore(tests): renamed rules to ruler to match the pkg name 2026-09-25 12:35:38 +05:30
Praneeth Lingam
f91bcaf3ac chore(tests): split alerts suite into alertmanager and rules 2026-09-25 12:35:28 +05:30
nikhilmantri0902
b506ec1627 feat(alert-channel-integrations): integration tests using wiremock for jsm ops
- testChannel suite: create+note success (GenieKey auth, HTML body, default
  tags/source), failed-note tolerance (404 note still delivers), permanent
  422/401 on create with the note never attempted
- firing-path suite: metrics/logs default-template alerts (HTML deep links),
  create 429 retry with a single note after the successful create
- webhook verification and journal lookups accept a path regex (path_pattern)
  for paths embedding the group-hash alias
2026-09-03 21:16:19 +05:30
nikhilmantri0902
7c1c298f6e feat(alert-channel-integrations): integration tests using wiremock for jira
- testChannel suite: create (JQL + basic auth + ADF panel/labels), wont-fix
  JQL clause, update-existing-open (labels preserved), reopen-done
  (transition selection), permanent 400/401 with no retry, service-account
  gateway routing via tenant_info cloud id
- firing-path suite: metrics/logs default-template issues (ADF deep links),
  search 429 retry with a single create
- wiremock aliases signoz-test.atlassian.net and api.atlassian.com; journal
  lookup extracted to fixtures as find_requests
2026-09-03 20:58:06 +05:30
nikhilmantri0902
160f0e0c70 feat(alert-channel-integrations): integration tests using wiremock for incident.io
- testChannel suite: 202 success (event shape + single Bearer prefix),
  permanent 401/422 with no retry
- firing-path suite: metrics/logs default-template events (markdown links,
  dedup key, source_url, metadata), 429->202 retry
- wiremock container now serves a multi-SAN cert and aliases api.incident.io
  alongside chat.googleapis.com; tls-hosts label recreates stale reused containers
- fix: clickhouse mounted configs moved out of pytest basetemp - a later
  session's basetemp wipe made clickhouse hot-reload config.d and drop the
  cluster definition on reused containers
2026-09-03 20:25:35 +05:30
nikhilmantri0902
1fbaccfe99 Merge branch 'main' into feat/google_chat_alert_integration_tests 2026-09-03 19:46:15 +05:30
Nikhil Mantri
06f8a07638 Merge branch 'main' into feat/google_chat_alert_integration_tests 2026-09-03 17:28:19 +05:30
Nikhil Mantri
80e0c2fe30 Merge branch 'main' into feat/google_chat_alert_integration_tests 2026-08-21 13:50:34 +05:30
nikhilmantri0902
5367f39b61 test(alertmanager): drop google chat threading assertions after threading removal 2026-08-19 01:08:08 +05:30
Nikhil Mantri
a86b20925b Merge branch 'main' into feat/google_chat_alert_integration_tests 2026-08-19 00:34:02 +05:30
nikhilmantri0902
8f89a1abf5 test: lower googlechat notification poll ceiling to 60s 2026-08-18 15:36:48 +05:30
nikhilmantri0902
7ecf6f4b60 test(googlechat): poll for org registration instead of a fixed sleep
wait_for_org_registration probes channels/test with a throwaway webhook
sentinel aimed at an unstubbed wiremock path: it 404s before reaching
any notifier until the org's alertmanager registers, and probe traffic
stays off the paths tests assert counts on. Replaces both sleep(12)s
in the firing-path suite; the poll returns instantly once registered.
2026-08-18 11:40:38 +05:30
nikhilmantri0902
709b1a6745 test(googlechat): split the retry scenario into its own test
The retry case needed name-keyed branching in the parametrized test
(stub lookup + a trailing post-check); as its own test both branches
go away and each test reads standalone.
2026-08-18 11:35:00 +05:30
Naman Verma
b11a633331 Merge branch 'main' into feat/google_chat_alert_integration_tests 2026-08-18 09:47:50 +05:30
nikhilmantri0902
e12d6f7a42 test(googlechat): address review feedback on assertions and flakiness
- switch testChannel calls to the non-deprecated /api/v1/channels/test
- pin failure cases to 500 + 'unexpected status code <N>' body instead
  of a weak != 204, distinguishing downstream 400 from 403
- replace the fixed sleep(10) with a poll: channels/test 404s until the
  org's alertmanager registers, without reaching the notifier, so the
  first non-404 response is the single authoritative delivery attempt
- parametrize testChannel cases via a NamedTuple with short ids
- firing path: exactly-once (count 1) guard on non-retry cases,
  threading params asserted on the retry delivery, and both retry
  attempts must share the same threadKey
2026-08-18 01:20:48 +05:30
nikhilmantri0902
aefe6e76ab test(googlechat): serve the wiremock mock over CA-issued TLS on 443
Adopt the integration-CA pattern from #12486: the notification_channel
wiremock now serves https on 443 as chat.googleapis.com with a cert
issued by the integration CA, replacing the self-signed 8443 +
insecure_skip_verify hack. Webhook urls mirror prod (implicit 443).

- recreate the container on CA rotation via the CA id label
- mount the CA into the alertmanager suite's signoz (it was missing,
  so firing-path delivery failed x509 verification)
- move gchat stub/card builders into fixtures/notification_channel.py
2026-08-17 23:53:14 +05:30
Nikhil Mantri
d27f94abce Merge branch 'main' into feat/google_chat_alert_integration_tests 2026-08-17 22:00:15 +05:30
nikhilmantri0902
7774544809 test(alert-channel-integrations): recreate stale notification_channel cache lacking https:8443 2026-08-06 15:55:56 +05:30
nikhilmantri0902
2f48fc8ef8 test(alert-channel-integrations): drop unused 8443 host mapping for google chat wiremock 2026-08-06 15:37:36 +05:30
nikhilmantri0902
946058210b test(alert-channel-integrations): ruff format google chat test 2026-08-06 15:04:46 +05:30
Nikhil Mantri
113943771a Merge branch 'main' into feat/google_chat_alert_integration_tests 2026-08-06 15:02:23 +05:30
nikhilmantri0902
3c517e5bde chore: added alert integration tests for google chat 2026-08-06 14:46:09 +05:30
127 changed files with 3828 additions and 4576 deletions

View File

@@ -68,7 +68,6 @@ jobs:
- semconvfamilies
- serviceaccount
- spanmapper
- tracedetail
- querier_json_body
- querier_skip_resource_fingerprint
- ttl

View File

@@ -1,44 +1,5 @@
components:
schemas:
AiobservabilitytypesMessage:
properties:
content:
items:
$ref: '#/components/schemas/AiobservabilitytypesPart'
type: array
finishReason:
type: string
role:
type: string
required:
- content
type: object
AiobservabilitytypesPart:
properties:
arguments: {}
content:
type: string
id:
type: string
isError:
type: boolean
name:
type: string
toolCallId:
type: string
type:
$ref: '#/components/schemas/AiobservabilitytypesPartType'
required:
- type
type: object
AiobservabilitytypesPartType:
enum:
- text
- thinking
- tool_call
- tool_result
- generic
type: string
AlertmanagertypesChannel:
properties:
createdAt:
@@ -9080,6 +9041,15 @@ components:
- alertType
- ruleType
type: object
RuletypesListableRuleViews:
properties:
views:
items:
$ref: '#/components/schemas/RuletypesRuleView'
type: array
required:
- views
type: object
RuletypesListableRules:
properties:
labels:
@@ -9177,6 +9147,16 @@ components:
- ruleType
- condition
type: object
RuletypesPostableRuleView:
properties:
data:
$ref: '#/components/schemas/RuletypesRuleViewData'
name:
type: string
required:
- name
- data
type: object
RuletypesQueryType:
enum:
- builder
@@ -9315,6 +9295,45 @@ components:
- promql_rule
- anomaly_rule
type: string
RuletypesRuleView:
properties:
createdAt:
format: date-time
type: string
data:
$ref: '#/components/schemas/RuletypesRuleViewData'
id:
type: string
name:
type: string
orgId:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- data
- orgId
type: object
RuletypesRuleViewData:
properties:
order:
$ref: '#/components/schemas/RuletypesListOrder'
query:
type: string
sort:
$ref: '#/components/schemas/RuletypesListSort'
states:
items:
type: string
type: array
version:
type: string
required:
- version
type: object
RuletypesScheduleType:
enum:
- hourly
@@ -9732,19 +9751,6 @@ components:
required:
- aggregations
type: object
SpantypesGettableTraceThread:
properties:
nextCursor:
type: string
prevCursor:
type: string
spans:
items:
$ref: '#/components/schemas/SpantypesThreadSpan'
type: array
required:
- spans
type: object
SpantypesGettableWaterfallTrace:
properties:
endTimestampMillis:
@@ -10059,92 +10065,6 @@ components:
nullable: true
type: object
type: object
SpantypesThreadSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
db_name:
type: string
db_operation:
type: string
duration_nano:
minimum: 0
type: integer
events:
items:
$ref: '#/components/schemas/SpantypesEvent'
nullable: true
type: array
external_http_method:
type: string
external_http_url:
type: string
flags:
minimum: 0
type: integer
formatted_input:
items:
$ref: '#/components/schemas/AiobservabilitytypesMessage'
type: array
formatted_output:
items:
$ref: '#/components/schemas/AiobservabilitytypesMessage'
type: array
has_children:
type: boolean
has_error:
type: boolean
http_host:
type: string
http_method:
type: string
http_url:
type: string
is_remote:
type: string
kind_string:
type: string
level:
minimum: 0
type: integer
name:
type: string
parent_span_id:
type: string
references:
items:
$ref: '#/components/schemas/SpantypesOtelSpanRef'
type: array
resource:
additionalProperties:
type: string
nullable: true
type: object
response_status_code:
type: string
span_id:
type: string
status_code:
type: integer
status_code_string:
type: string
status_message:
type: string
sub_tree_node_count:
minimum: 0
type: integer
time_unix:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
type: string
required:
- references
type: object
SpantypesUpdatableSpanMapper:
properties:
config:
@@ -15869,90 +15789,6 @@ paths:
tags:
- tracedetail
x-signoz-stability: alpha
/api/v1/traces/{traceID}/thread:
get:
deprecated: false
description: Returns the spans carrying gen_ai input or output messages in timestamp
order, each with the messages normalised into formatted_input and formatted_output.
Pass nextCursor as after or prevCursor as before to page, or spanId to open
the page around a span.
operationId: GetTraceThread
parameters:
- in: query
name: limit
schema:
type: integer
- in: query
name: after
schema:
type: string
- in: query
name: before
schema:
type: string
- in: query
name: spanId
schema:
type: string
- in: path
name: traceID
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableTraceThread'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get thread view for a trace
tags:
- tracedetail
x-signoz-stability: alpha
/api/v1/user/me:
get:
deprecated: true
@@ -21415,6 +21251,235 @@ paths:
tags:
- users
x-signoz-stability: alpha
/api/v2/rule_views:
get:
deprecated: false
description: Returns every saved view in the calling user's org. Saved views
are shared org-wide.
operationId: ListRuleViews
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesListableRuleViews'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List rule saved views
tags:
- rules
x-signoz-stability: alpha
post:
deprecated: false
description: Persists the calling user's rule listing state (query, states,
sort, order) as a named, reusable view shared across the org.
operationId: CreateRuleView
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostableRuleView'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesRuleView'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Create rule saved view
tags:
- rules
x-signoz-stability: alpha
/api/v2/rule_views/{id}:
delete:
deprecated: false
description: Removes a saved view. Saved views are shared org-wide. Deleting
a non-existent view returns 404.
operationId: DeleteRuleView
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Delete rule saved view
tags:
- rules
x-signoz-stability: alpha
put:
deprecated: false
description: Replaces a saved view's name and data. Saved views are shared org-wide.
operationId: UpdateRuleView
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostableRuleView'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesRuleView'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Update rule saved view
tags:
- rules
x-signoz-stability: alpha
/api/v2/rules:
get:
deprecated: true

View File

@@ -19,7 +19,9 @@ import type {
import type {
CreateRule201,
CreateRuleView201,
DeleteRuleByIDPathParameters,
DeleteRuleViewPathParameters,
GetRuleByID200,
GetRuleByIDPathParameters,
GetRuleHistoryFilterKeys200,
@@ -40,6 +42,7 @@ import type {
GetRuleHistoryTopContributors200,
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
ListRuleViews200,
ListRules200,
ListRulesV3200,
ListRulesV3Params,
@@ -47,8 +50,11 @@ import type {
PatchRuleByIDPathParameters,
RenderErrorResponseDTO,
RuletypesPostableRuleDTO,
RuletypesPostableRuleViewDTO,
TestRule200,
UpdateRuleByIDPathParameters,
UpdateRuleView200,
UpdateRuleViewPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -74,6 +80,351 @@ const withQueryKey = <T extends object, K>(
return result;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List rule saved views
*/
export const listRuleViews = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListRuleViews200>({
url: `/api/v2/rule_views`,
method: 'GET',
signal,
});
};
export const getListRuleViewsQueryKey = () => {
return [`/api/v2/rule_views`] as const;
};
export const getListRuleViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listRuleViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listRuleViews>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListRuleViewsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRuleViews>>> = ({
signal,
}) => listRuleViews(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listRuleViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListRuleViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listRuleViews>>
>;
export type ListRuleViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List rule saved views
*/
export function useListRuleViews<
TData = Awaited<ReturnType<typeof listRuleViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listRuleViews>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListRuleViewsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary List rule saved views
*/
export const invalidateListRuleViews = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListRuleViewsQueryKey() },
options,
);
return queryClient;
};
/**
* Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.
* @summary Create rule saved view
*/
export const createRuleView = (
ruletypesPostableRuleViewDTO?: BodyType<RuletypesPostableRuleViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRuleView201>({
url: `/api/v2/rule_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleViewDTO,
signal,
});
};
export const getCreateRuleViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
> => {
const mutationKey = ['createRuleView'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRuleView>>,
{ data?: BodyType<RuletypesPostableRuleViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createRuleView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateRuleViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createRuleView>>
>;
export type CreateRuleViewMutationBody =
| BodyType<RuletypesPostableRuleViewDTO>
| undefined;
export type CreateRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create rule saved view
*/
export const useCreateRuleView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
> => {
return useMutation(getCreateRuleViewMutationOptions(options));
};
/**
* Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.
* @summary Delete rule saved view
*/
export const deleteRuleView = (
{ id }: DeleteRuleViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/rule_views/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteRuleViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
> => {
const mutationKey = ['deleteRuleView'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteRuleView>>,
{ pathParams: DeleteRuleViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteRuleView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteRuleViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteRuleView>>
>;
export type DeleteRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete rule saved view
*/
export const useDeleteRuleView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
> => {
return useMutation(getDeleteRuleViewMutationOptions(options));
};
/**
* Replaces a saved view's name and data. Saved views are shared org-wide.
* @summary Update rule saved view
*/
export const updateRuleView = (
{ id }: UpdateRuleViewPathParameters,
ruletypesPostableRuleViewDTO?: BodyType<RuletypesPostableRuleViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateRuleView200>({
url: `/api/v2/rule_views/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleViewDTO,
signal,
});
};
export const getUpdateRuleViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
> => {
const mutationKey = ['updateRuleView'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateRuleView>>,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRuleView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRuleViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRuleView>>
>;
export type UpdateRuleViewMutationBody =
| BodyType<RuletypesPostableRuleViewDTO>
| undefined;
export type UpdateRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update rule saved view
*/
export const useUpdateRuleView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
> => {
return useMutation(getUpdateRuleViewMutationOptions(options));
};
/**
* This endpoint lists all alert rules with their current evaluation state. Deprecated: use ListRulesV3, which supports filtering, sorting and pagination.
* @deprecated

View File

@@ -4,53 +4,6 @@
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
export enum AiobservabilitytypesPartTypeDTO {
text = 'text',
thinking = 'thinking',
tool_call = 'tool_call',
tool_result = 'tool_result',
generic = 'generic',
}
export interface AiobservabilitytypesPartDTO {
arguments?: unknown;
/**
* @type string
*/
content?: string;
/**
* @type string
*/
id?: string;
/**
* @type boolean
*/
isError?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
toolCallId?: string;
type: AiobservabilitytypesPartTypeDTO;
}
export interface AiobservabilitytypesMessageDTO {
/**
* @type array
*/
content: AiobservabilitytypesPartDTO[];
/**
* @type string
*/
finishReason?: string;
/**
* @type string
*/
role?: string;
}
export interface AlertmanagertypesChannelDTO {
/**
* @type string
@@ -10375,6 +10328,56 @@ export interface RuletypesListableRuleDTO {
updatedBy?: string;
}
export interface RuletypesRuleViewDataDTO {
order?: RuletypesListOrderDTO;
/**
* @type string
*/
query?: string;
sort?: RuletypesListSortDTO;
/**
* @type array
*/
states?: string[];
/**
* @type string
*/
version: string;
}
export interface RuletypesRuleViewDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
data: RuletypesRuleViewDataDTO;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface RuletypesListableRuleViewsDTO {
/**
* @type array
*/
views: RuletypesRuleViewDTO[];
}
export interface RuletypesListableRulesDTO {
/**
* @type array
@@ -10543,6 +10546,14 @@ export interface RuletypesPostableRuleDTO {
version?: string;
}
export interface RuletypesPostableRuleViewDTO {
data: RuletypesRuleViewDataDTO;
/**
* @type string
*/
name: string;
}
export type RuletypesRuleDTOAnnotations = { [key: string]: string };
export type RuletypesRuleDTOLabels = { [key: string]: string };
@@ -11256,169 +11267,6 @@ export interface SpantypesOtelSpanRefDTO {
traceId?: string;
}
export type SpantypesThreadSpanDTOAttributesAnyOf = { [key: string]: unknown };
/**
* @nullable
*/
export type SpantypesThreadSpanDTOAttributes =
SpantypesThreadSpanDTOAttributesAnyOf | null;
export type SpantypesThreadSpanDTOResourceAnyOf = { [key: string]: string };
/**
* @nullable
*/
export type SpantypesThreadSpanDTOResource =
SpantypesThreadSpanDTOResourceAnyOf | null;
export interface SpantypesThreadSpanDTO {
/**
* @type object,null
*/
attributes?: SpantypesThreadSpanDTOAttributes;
/**
* @type string
*/
db_name?: string;
/**
* @type string
*/
db_operation?: string;
/**
* @type integer
* @minimum 0
*/
duration_nano?: number;
/**
* @type array,null
*/
events?: SpantypesEventDTO[] | null;
/**
* @type string
*/
external_http_method?: string;
/**
* @type string
*/
external_http_url?: string;
/**
* @type integer
* @minimum 0
*/
flags?: number;
/**
* @type array
*/
formatted_input?: AiobservabilitytypesMessageDTO[];
/**
* @type array
*/
formatted_output?: AiobservabilitytypesMessageDTO[];
/**
* @type boolean
*/
has_children?: boolean;
/**
* @type boolean
*/
has_error?: boolean;
/**
* @type string
*/
http_host?: string;
/**
* @type string
*/
http_method?: string;
/**
* @type string
*/
http_url?: string;
/**
* @type string
*/
is_remote?: string;
/**
* @type string
*/
kind_string?: string;
/**
* @type integer
* @minimum 0
*/
level?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
parent_span_id?: string;
/**
* @type array
*/
references: SpantypesOtelSpanRefDTO[];
/**
* @type object,null
*/
resource?: SpantypesThreadSpanDTOResource;
/**
* @type string
*/
response_status_code?: string;
/**
* @type string
*/
span_id?: string;
/**
* @type integer
*/
status_code?: number;
/**
* @type string
*/
status_code_string?: string;
/**
* @type string
*/
status_message?: string;
/**
* @type integer
* @minimum 0
*/
sub_tree_node_count?: number;
/**
* @type integer
* @minimum 0
*/
time_unix?: number;
/**
* @type string
*/
trace_id?: string;
/**
* @type string
*/
trace_state?: string;
}
export interface SpantypesGettableTraceThreadDTO {
/**
* @type string
*/
nextCursor?: string;
/**
* @type string
*/
prevCursor?: string;
/**
* @type array
*/
spans: SpantypesThreadSpanDTO[];
}
export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
@@ -13092,40 +12940,6 @@ export type GetTraceAggregations200 = {
status: string;
};
export type GetTraceThreadPathParameters = {
traceID: string;
};
export type GetTraceThreadParams = {
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type string
* @description undefined
*/
after?: string;
/**
* @type string
* @description undefined
*/
before?: string;
/**
* @type string
* @description undefined
*/
spanId?: string;
};
export type GetTraceThread200 = {
data: SpantypesGettableTraceThreadDTO;
/**
* @type string
*/
status: string;
};
export type ListUserPreferences200 = {
/**
* @type array
@@ -14027,6 +13841,36 @@ export type GetUsersByRoleID200 = {
status: string;
};
export type ListRuleViews200 = {
data: RuletypesListableRuleViewsDTO;
/**
* @type string
*/
status: string;
};
export type CreateRuleView201 = {
data: RuletypesRuleViewDTO;
/**
* @type string
*/
status: string;
};
export type DeleteRuleViewPathParameters = {
id: string;
};
export type UpdateRuleViewPathParameters = {
id: string;
};
export type UpdateRuleView200 = {
data: RuletypesRuleViewDTO;
/**
* @type string
*/
status: string;
};
export type ListRules200 = {
/**
* @type array

View File

@@ -4,17 +4,11 @@
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import { useMutation } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
@@ -22,9 +16,6 @@ import type {
GetFlamegraphPathParameters,
GetTraceAggregations200,
GetTraceAggregationsPathParameters,
GetTraceThread200,
GetTraceThreadParams,
GetTraceThreadPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
@@ -36,26 +27,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Computes span aggregations grouped by requested field.
* @summary Get aggregations for a trace
@@ -156,121 +127,6 @@ export const useGetTraceAggregations = <
> => {
return useMutation(getGetTraceAggregationsMutationOptions(options));
};
/**
* Returns the spans carrying gen_ai input or output messages in timestamp order, each with the messages normalised into formatted_input and formatted_output. Pass nextCursor as after or prevCursor as before to page, or spanId to open the page around a span.
* @summary Get thread view for a trace
*/
export const getTraceThread = (
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetTraceThread200>({
url: `/api/v1/traces/${traceID}/thread`,
method: 'GET',
params,
signal,
});
};
export const getGetTraceThreadQueryKey = (
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
) => {
return [
`/api/v1/traces/${traceID}/thread`,
...(params ? [params] : []),
] as const;
};
export const getGetTraceThreadQueryOptions = <
TData = Awaited<ReturnType<typeof getTraceThread>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getTraceThread>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetTraceThreadQueryKey({ traceID }, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getTraceThread>>> = ({
signal,
}) => getTraceThread({ traceID }, params, signal);
return {
queryKey,
queryFn,
enabled: traceID !== null && traceID !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getTraceThread>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetTraceThreadQueryResult = NonNullable<
Awaited<ReturnType<typeof getTraceThread>>
>;
export type GetTraceThreadQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get thread view for a trace
*/
export function useGetTraceThread<
TData = Awaited<ReturnType<typeof getTraceThread>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getTraceThread>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetTraceThreadQueryOptions(
{ traceID },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get thread view for a trace
*/
export const invalidateGetTraceThread = async (
queryClient: QueryClient,
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetTraceThreadQueryKey({ traceID }, params) },
options,
);
return queryClient;
};
/**
* Returns the flamegraph view of spans for a given trace ID.
* @summary Get flamegraph view for a trace

View File

@@ -5,22 +5,9 @@
}
.banner {
display: flex;
align-items: flex-start;
gap: var(--spacing-5);
padding: var(--spacing-6) var(--spacing-8);
border-radius: var(--radius-2);
border: 1px solid color-mix(in srgb, var(--bg-amber-400) 30%, transparent);
background: color-mix(in srgb, var(--bg-amber-400) 8%, transparent);
color: var(--bg-amber-300, var(--bg-amber-400));
margin-top: var(--spacing-4);
}
.bannerIcon {
flex-shrink: 0;
margin-top: var(--spacing-1);
}
.error {
padding: var(--spacing-6) var(--spacing-8);
border-radius: var(--radius-2);

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Callout } from '@signozhq/ui/callout';
import { Typography } from '@signozhq/ui/typography';
import { TriangleAlert } from '@signozhq/icons';
import { useListUnmappedLLMModels } from 'api/generated/services/llmpricingrules';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
@@ -80,13 +80,10 @@ function UnpricedModelsTab(): JSX.Element {
return (
<div className={styles.unpricedModelsTab}>
<div className={styles.banner}>
<TriangleAlert size="sm" className={styles.bannerIcon} />
<Typography.Text as="span" size="small" color="warning">
Models detected in traces without pricing. Map each to a billing model or
create pricing so estimated cost can be computed.
</Typography.Text>
</div>
<Callout type="warning" size="small" showIcon className={styles.banner}>
Models detected in traces without pricing. Map each to a billing model or
create pricing so estimated cost can be computed.
</Callout>
{isError && (
<div className={styles.error}>

46
go.mod
View File

@@ -73,18 +73,18 @@ require (
go.opentelemetry.io/contrib/config v0.10.0
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/otel v1.45.0
go.opentelemetry.io/otel/metric v1.45.0
go.opentelemetry.io/otel/sdk v1.45.0
go.opentelemetry.io/otel/trace v1.45.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.52.0
golang.org/x/crypto v0.55.0
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/net v0.55.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/text v0.37.0
golang.org/x/sync v0.22.0
golang.org/x/text v0.41.0
gonum.org/v1/gonum v0.17.0
google.golang.org/api v0.272.0
google.golang.org/protobuf v1.36.11
@@ -151,7 +151,7 @@ require (
go.opentelemetry.io/collector/pdata/xpdata v0.148.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/term v0.45.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
modernc.org/libc v1.70.0 // indirect
@@ -164,7 +164,7 @@ require (
)
require (
cel.dev/expr v0.25.1 // indirect
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
@@ -197,7 +197,7 @@ require (
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.24.2 // indirect
@@ -222,7 +222,7 @@ require (
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.5 // indirect
@@ -360,27 +360,27 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.60.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0
go.opentelemetry.io/otel/sdk/metric v1.45.0
go.opentelemetry.io/proto/otlp v1.11.0
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478
google.golang.org/grpc v1.82.1 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d
google.golang.org/grpc v1.83.2 // indirect
gopkg.in/telebot.v3 v3.3.8 // indirect
k8s.io/client-go v0.35.3 // indirect
k8s.io/klog/v2 v2.140.0 // indirect

94
go.sum
View File

@@ -1,5 +1,5 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -336,8 +336,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -549,8 +549,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajR
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
@@ -1255,8 +1255,8 @@ go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLb
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/contrib/zpages v0.63.0 h1:TppOKuZGbqXMgsfjqq3i09N5Vbo1JLtLImUqiTPGnX4=
go.opentelemetry.io/contrib/zpages v0.63.0/go.mod h1:5F8uugz75ay/MMhRRhxAXY33FuaI8dl7jTxefrIy5qk=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
@@ -1265,12 +1265,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
@@ -1283,21 +1283,23 @@ go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIE
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A=
go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM=
go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk=
go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ=
@@ -1337,8 +1339,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1376,8 +1378,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1425,8 +1427,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1461,8 +1463,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1550,12 +1552,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1566,8 +1568,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1630,8 +1632,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1772,10 +1774,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1808,8 +1810,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View File

@@ -132,6 +132,64 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListRuleViews), handler.OpenAPIDef{
ID: "ListRuleViews",
Tags: []string{"rules"},
Summary: "List rule saved views",
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
Response: new(ruletypes.ListableRuleViews),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.CreateRuleView), handler.OpenAPIDef{
ID: "CreateRuleView",
Tags: []string{"rules"},
Summary: "Create rule saved view",
Description: "Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.",
Request: new(ruletypes.PostableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.RuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.UpdateRuleView), handler.OpenAPIDef{
ID: "UpdateRuleView",
Tags: []string{"rules"},
Summary: "Update rule saved view",
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
Request: new(ruletypes.UpdatableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.RuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.DeleteRuleView), handler.OpenAPIDef{
ID: "DeleteRuleView",
Tags: []string{"rules"},
Summary: "Delete rule saved view",
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
ID: "ListDowntimeSchedules",
Tags: []string{"downtimeschedules"},

View File

@@ -67,23 +67,5 @@ func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/traces/{traceID}/thread", handler.New(
provider.authzMiddleware.ViewAccess(provider.traceDetailHandler.GetThread),
handler.OpenAPIDef{
ID: "GetTraceThread",
Tags: []string{"tracedetail"},
Summary: "Get thread view for a trace",
Description: "Returns the spans carrying gen_ai input or output messages in timestamp order, each with the messages normalised into formatted_input and formatted_output. Pass nextCursor as after or prevCursor as before to page, or spanId to open the page around a span.",
RequestQuery: new(spantypes.QueryableThread),
Response: new(spantypes.GettableTraceThread),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -58,7 +58,9 @@ func New(ctx context.Context, cfg Config, build version.Build, serviceName strin
// Prepare the resource configuration by merging
// resource and attributes.
sch := semconv.SchemaURL
// contribsdkconfig merges this resource with sdkresource.Default().
// The merge fails if the schema URLs are different.
sch := sdkresource.Default().SchemaURL()
configResource := contribsdkconfig.Resource{
Attributes: mergeAttributes(cfg.Resource.Attributes, resource),
Detectors: nil,

View File

@@ -75,25 +75,3 @@ func (h *handler) GetFlamegraph(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, result)
}
func (h *handler) GetThread(rw http.ResponseWriter, r *http.Request) {
req := new(spantypes.QueryableThread)
if err := binding.Query.BindQuery(r.URL.Query(), req); err != nil {
render.Error(rw, err)
return
}
query, err := spantypes.NewThreadQuery(req)
if err != nil {
render.Error(rw, err)
return
}
result, err := h.module.GetThread(r.Context(), mux.Vars(r)["traceID"], query)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, result)
}

View File

@@ -173,49 +173,6 @@ func (m *module) getWindowedWaterfall(ctx context.Context, traceID, selectedSpan
), nil
}
func (m *module) GetThread(ctx context.Context, traceID string, query *spantypes.ThreadQuery) (*spantypes.GettableTraceThread, error) {
summary, err := m.store.GetTraceSummary(ctx, traceID)
if err != nil {
return nil, err
}
// One extra row per side signals a next/prev page; NewGettableTraceThread trims the response back to query.Limit.
page := spantypes.ThreadPage{Limit: query.Limit + 1}
switch {
case query.SpanID != "":
// Window centred on the span: fetch both directions, NewGettableTraceThread splits the limit.
anchor, err := m.store.GetThreadCursor(ctx, traceID, summary, query.SpanID)
if err != nil {
return nil, err
}
before, err := m.store.GetThreadSpans(ctx, traceID, summary, spantypes.ThreadPage{Cursor: anchor, From: spantypes.ThreadBefore, Limit: page.Limit})
if err != nil {
return nil, err
}
// ThreadAt keeps the anchor when it carries messages; otherwise the filter drops it.
after, err := m.store.GetThreadSpans(ctx, traceID, summary, spantypes.ThreadPage{Cursor: anchor, From: spantypes.ThreadAt, Limit: page.Limit})
if err != nil {
return nil, err
}
return spantypes.NewGettableTraceThread(traceID, query, before, after), nil
case query.Before != nil:
page.Cursor, page.From = query.Before, spantypes.ThreadBefore
before, err := m.store.GetThreadSpans(ctx, traceID, summary, page)
if err != nil {
return nil, err
}
return spantypes.NewGettableTraceThread(traceID, query, before, nil), nil
default:
// nil for the first page.
page.Cursor = query.After
after, err := m.store.GetThreadSpans(ctx, traceID, summary, page)
if err != nil {
return nil, err
}
return spantypes.NewGettableTraceThread(traceID, query, nil, after), nil
}
}
func (m *module) getFullFlamegraph(ctx context.Context, traceID string, summary *spantypes.TraceSummary, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error) {
fullSpans, err := m.store.GetFlamegraphSpans(ctx, traceID, summary.Start, summary.End, nil)
if err != nil {

View File

@@ -11,23 +11,12 @@ import (
"github.com/SigNoz/signoz/pkg/clickhousesql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const colServiceName = `resource_string_service$$$$name` // $ gets escaped so $$$$ converts to $$.
var fullSpanColumns = []string{
"duration_nano", "span_id", "has_error", "kind",
colServiceName, "name",
"attributes_string", "attributes_number", "attributes_bool", "resources_string",
"events", "status_message", "status_code_string", "kind_string", "parent_span_id",
"flags", "is_remote", "trace_state", "status_code",
"db_name", "db_operation", "http_method", "http_url", "http_host",
"external_http_method", "external_http_url", "response_status_code", "links as references",
}
func buildFieldExpr(fieldKey telemetrytypes.TelemetryFieldKey) (string, error) {
switch fieldKey.FieldContext {
case telemetrytypes.FieldContextResource:
@@ -134,8 +123,16 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta
return []spantypes.StorableSpan{}, nil
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("DISTINCT ON (span_id) timestamp")
sb.SelectMore(fullSpanColumns...)
sb.Select(
"DISTINCT ON (span_id) timestamp",
"duration_nano", "span_id", "has_error", "kind",
colServiceName, "name",
"attributes_string", "attributes_number", "attributes_bool", "resources_string",
"events", "status_message", "status_code_string", "kind_string", "parent_span_id",
"flags", "is_remote", "trace_state", "status_code",
"db_name", "db_operation", "http_method", "http_url", "http_host",
"external_http_method", "external_http_url", "response_status_code", "links as references",
)
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
ids := make([]any, len(spanIDs))
for i, id := range spanIDs {
@@ -158,81 +155,6 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta
return spans, nil
}
func (s *traceStore) GetThreadSpans(ctx context.Context, traceID string, summary *spantypes.TraceSummary, page spantypes.ThreadPage) ([]spantypes.StorableSpan, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("DISTINCT ON (span_id) timestamp")
sb.SelectMore(fullSpanColumns...)
sb.SelectMore("attributes")
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
sb.Where(
sb.E("trace_id", traceID),
sb.GE("ts_bucket_start", summary.Start.Unix()-1800),
sb.LE("ts_bucket_start", summary.End.Unix()),
// Reads only the JSON column; spans with messages only in the legacy maps are skipped.
// todo(nitya): pick the column from the attribute evolution metadata.
sb.Or(
sqlbuilder.Escape(fmt.Sprintf("attributes.%s IS NOT NULL", clickhousesql.Identifier(aiobservabilitytypes.GenAIInputMessages))),
sqlbuilder.Escape(fmt.Sprintf("attributes.%s IS NOT NULL", clickhousesql.Identifier(aiobservabilitytypes.GenAIOutputMessages))),
),
)
if cursor := page.Cursor; cursor != nil {
// ClickHouse can't use an index for a tuple comparison, so the separate timestamp and
// ts_bucket_start bounds are what skip the data on the far side of the cursor.
key := "(toUnixTimestamp64Nano(timestamp), span_id)"
bucket := int64(cursor.TimeUnixNano / uint64(time.Second))
timestamp := fmt.Sprintf("fromUnixTimestamp64Nano(toInt64(%s))", sb.Var(cursor.TimeUnixNano))
tuple := sqlbuilder.Tuple(cursor.TimeUnixNano, cursor.SpanID)
switch page.From {
case spantypes.ThreadBefore:
sb.Where(sb.LE("ts_bucket_start", bucket), "timestamp <= "+timestamp, sb.LT(key, tuple))
case spantypes.ThreadAt:
sb.Where(sb.GE("ts_bucket_start", bucket-1800), "timestamp >= "+timestamp, sb.GE(key, tuple))
default:
sb.Where(sb.GE("ts_bucket_start", bucket-1800), "timestamp >= "+timestamp, sb.GT(key, tuple))
}
}
// span_id breaks timestamp ties so the order matches the cursor key; otherwise tied spans
// can be skipped or repeated across pages.
if page.From == spantypes.ThreadBefore {
sb.OrderByDesc("timestamp")
sb.OrderByDesc("span_id")
} else {
sb.OrderByAsc("timestamp")
sb.OrderByAsc("span_id")
}
sb.Limit(page.Limit)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var spans []spantypes.StorableSpan
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &spans, query, args...); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying thread spans")
}
return spans, nil
}
func (s *traceStore) GetThreadCursor(ctx context.Context, traceID string, summary *spantypes.TraceSummary, spanID string) (*spantypes.ThreadCursor, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("toUnixTimestamp64Nano(timestamp)")
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
sb.Where(
sb.E("trace_id", traceID),
sb.GE("ts_bucket_start", summary.Start.Unix()-1800),
sb.LE("ts_bucket_start", summary.End.Unix()),
sb.E("span_id", spanID),
)
sb.Limit(1)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var timeUnixNano int64
if err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(&timeUnixNano); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.NewNotFoundf(spantypes.ErrCodeThreadSpanNotFound, "span %s not found in trace %s", spanID, traceID)
}
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying thread span")
}
return &spantypes.ThreadCursor{TimeUnixNano: uint64(timeUnixNano), SpanID: spanID}, nil
}
func (s *traceStore) GetFlamegraphSpans(ctx context.Context, traceID string, start, end time.Time, spanIDs []string) ([]spantypes.StorableSpan, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(

View File

@@ -13,7 +13,6 @@ type Handler interface {
GetWaterfallV4(http.ResponseWriter, *http.Request)
GetTraceAggregations(http.ResponseWriter, *http.Request)
GetFlamegraph(http.ResponseWriter, *http.Request)
GetThread(http.ResponseWriter, *http.Request)
}
// Module defines the business logic for trace detail operations.
@@ -21,5 +20,4 @@ type Module interface {
GetWaterfallV4(ctx context.Context, traceID string, selectedSpanID string, uncollapsedSpans []string) (*spantypes.GettableWaterfallTrace, error)
GetTraceAggregations(ctx context.Context, traceID string, req *spantypes.PostableTraceAggregations) (*spantypes.GettableTraceAggregations, error)
GetFlamegraph(ctx context.Context, traceID string, selectedSpanID string, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error)
GetThread(ctx context.Context, traceID string, query *spantypes.ThreadQuery) (*spantypes.GettableTraceThread, error)
}

View File

@@ -566,6 +566,24 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
}, nil
}
// flattenJSONPaths flattens a decoded JSON document into dotted keys, overwriting existing keys in out.
func flattenJSONPaths(prefix string, m map[string]any, out map[string]any) {
for k, v := range m {
key := k
if prefix != "" {
key = prefix + "." + k
}
switch child := v.(type) {
case map[string]any:
flattenJSONPaths(key, child, out)
case telemetrystoretypes.JSONValue:
flattenJSONPaths(key, child, out)
default:
out[key] = v
}
}
}
// mergeSpanAttributeColumns merges (attributes_string, attributes_number, attributes_bool, resources_string) into
// unified "attributes" and "resource" keys, and parses the stringified `events`
// and `links` columns into structured slices. Raw DB columns are removed.
@@ -580,7 +598,7 @@ func mergeSpanAttributeColumns(data map[string]any) {
resStr, hasRes := data["resources_string"]
if hasStr || hasNum || hasBool || attrJSON != nil || hasRes {
attributes := make(map[string]any)
attrJSON.FlattenInto("", attributes)
flattenJSONPaths("", attrJSON, attributes)
if m, ok := attrStr.(map[string]string); ok {
for k, v := range m {
attributes[k] = v

View File

@@ -36,7 +36,7 @@ func TestManager_ListRules_ValidatesParams(t *testing.T) {
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{Limit: -1})
require.ErrorContains(t, err, "invalid limit")
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{States: []string{"bogus"}})
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{ListFilter: ruletypes.ListFilter{States: []string{"bogus"}}})
require.ErrorContains(t, err, `invalid state "bogus"`)
}

View File

@@ -12,6 +12,11 @@ type Handler interface {
PatchRuleByID(http.ResponseWriter, *http.Request)
TestRule(http.ResponseWriter, *http.Request)
ListRuleViews(http.ResponseWriter, *http.Request)
CreateRuleView(http.ResponseWriter, *http.Request)
UpdateRuleView(http.ResponseWriter, *http.Request)
DeleteRuleView(http.ResponseWriter, *http.Request)
ListDowntimeSchedules(http.ResponseWriter, *http.Request)
GetDowntimeScheduleByID(http.ResponseWriter, *http.Request)
CreateDowntimeSchedule(http.ResponseWriter, *http.Request)

View File

@@ -49,4 +49,9 @@ type Ruler interface {
// TODO: expose downtime CRUD as methods on Ruler directly instead of leaking the
// store interface. The handler should not call store methods directly.
MaintenanceStore() alertmanagertypes.MaintenanceStore
CreateRuleView(ctx context.Context, orgID valuer.UUID, postable ruletypes.PostableRuleView) (*ruletypes.RuleView, error)
ListRuleViews(ctx context.Context, orgID valuer.UUID) (*ruletypes.ListableRuleViews, error)
UpdateRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable ruletypes.UpdatableRuleView) (*ruletypes.RuleView, error)
DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
}

View File

@@ -0,0 +1,93 @@
package sqlrulestore
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (r *rule) CreateRuleView(ctx context.Context, view *ruletypes.RuleView) error {
_, err := r.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(view).
Exec(ctx)
if err != nil {
return r.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "rule view with id %s already exists", view.ID)
}
return nil
}
func (r *rule) GetRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*ruletypes.RuleView, error) {
view := new(ruletypes.RuleView)
err := r.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(view).
Where("id = ?", id).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", id)
}
return view, nil
}
func (r *rule) ListRuleViews(ctx context.Context, orgID valuer.UUID) ([]*ruletypes.RuleView, error) {
views := make([]*ruletypes.RuleView, 0)
err := r.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&views).
Where("org_id = ?", orgID).
OrderExpr("updated_at DESC").
Scan(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't list rule views")
}
return views, nil
}
func (r *rule) UpdateRuleView(ctx context.Context, view *ruletypes.RuleView) error {
res, err := r.sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(view).
WherePK().
Where("org_id = ?", view.OrgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't update rule view")
}
rows, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read rule view update result")
}
if rows == 0 {
return errors.Newf(errors.TypeNotFound, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", view.ID)
}
return nil
}
func (r *rule) DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
res, err := r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(ruletypes.RuleView)).
Where("id = ?", id).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't delete rule view")
}
rows, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read rule view delete result")
}
if rows == 0 {
return errors.Newf(errors.TypeNotFound, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", id)
}
return nil
}

View File

@@ -345,3 +345,122 @@ func (handler *handler) DeleteDowntimeScheduleByID(rw http.ResponseWriter, req *
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) ListRuleViews(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
views, err := handler.ruler.ListRuleViews(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, views)
}
func (handler *handler) CreateRuleView(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
var postable ruletypes.PostableRuleView
if err := binding.JSON.BindBody(req.Body, &postable); err != nil {
render.Error(rw, err)
return
}
view, err := handler.ruler.CreateRuleView(ctx, orgID, postable)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, view)
}
func (handler *handler) UpdateRuleView(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
return
}
var updatable ruletypes.UpdatableRuleView
if err := binding.JSON.BindBody(req.Body, &updatable); err != nil {
render.Error(rw, err)
return
}
view, err := handler.ruler.UpdateRuleView(ctx, orgID, id, updatable)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, view)
}
func (handler *handler) DeleteRuleView(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
return
}
if err := handler.ruler.DeleteRuleView(ctx, orgID, id); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@@ -147,3 +147,41 @@ func (provider *provider) TestNotification(ctx context.Context, orgID valuer.UUI
func (provider *provider) MaintenanceStore() alertmanagertypes.MaintenanceStore {
return provider.manager.MaintenanceStore()
}
func (provider *provider) CreateRuleView(ctx context.Context, orgID valuer.UUID, postable ruletypes.PostableRuleView) (*ruletypes.RuleView, error) {
if err := postable.Validate(); err != nil {
return nil, err
}
view := postable.NewRuleView(orgID)
if err := provider.ruleStore.CreateRuleView(ctx, view); err != nil {
return nil, err
}
return view, nil
}
func (provider *provider) ListRuleViews(ctx context.Context, orgID valuer.UUID) (*ruletypes.ListableRuleViews, error) {
views, err := provider.ruleStore.ListRuleViews(ctx, orgID)
if err != nil {
return nil, err
}
return &ruletypes.ListableRuleViews{Views: views}, nil
}
func (provider *provider) UpdateRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable ruletypes.UpdatableRuleView) (*ruletypes.RuleView, error) {
if err := updatable.Validate(); err != nil {
return nil, err
}
view, err := provider.ruleStore.GetRuleView(ctx, orgID, id)
if err != nil {
return nil, err
}
view.Update(updatable)
if err := provider.ruleStore.UpdateRuleView(ctx, view); err != nil {
return nil, err
}
return view, nil
}
func (provider *provider) DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return provider.ruleStore.DeleteRuleView(ctx, orgID, id)
}

View File

@@ -259,6 +259,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddAIObservabilityQuickFiltersFactory(sqlstore),
sqlmigration.NewAddChannelSpecFactory(sqlschema),
sqlmigration.NewAddUserTuplesFactory(sqlstore),
sqlmigration.NewAddRuleViewFactory(sqlstore, sqlschema),
)
}

View File

@@ -0,0 +1,78 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addRuleView struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddRuleViewFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_rule_view"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addRuleView{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
})
}
func (migration *addRuleView) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addRuleView) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
sqls := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "rule_view",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "data", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if _, err := tx.NewCreateIndex().
Table("rule_view").
Column("org_id").
Index("idx_rule_view_org_id").
IfNotExists().
Exec(ctx); err != nil {
return err
}
return tx.Commit()
}
func (migration *addRuleView) Down(_ context.Context, _ *bun.DB) error {
return nil
}

View File

@@ -1,41 +0,0 @@
package genaimessages
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testdata/captured/<sdk>/<attribute>.txt holds a real attribute value from the scripts
// repo's static-telemetry-generator captures; <attribute>.want.json is its reviewed output.
func TestNormalizeCapturedPayloads(t *testing.T) {
inputs, err := filepath.Glob(filepath.Join("testdata", "captured", "*", "*.txt"))
require.NoError(t, err)
require.NotEmpty(t, inputs)
for _, input := range inputs {
sdk := filepath.Base(filepath.Dir(input))
attribute := strings.TrimSuffix(filepath.Base(input), ".txt")
t.Run(sdk+"/"+attribute, func(t *testing.T) {
raw, err := os.ReadFile(input)
require.NoError(t, err)
wantJSON, err := os.ReadFile(strings.TrimSuffix(input, ".txt") + ".want.json")
require.NoError(t, err)
var want []aiobservabilitytypes.Message
require.NoError(t, json.Unmarshal(wantJSON, &want))
got, err := json.Marshal(Normalize(string(raw)))
require.NoError(t, err)
var gotRoundTrip []aiobservabilitytypes.Message
require.NoError(t, json.Unmarshal(got, &gotRoundTrip))
assert.Equal(t, want, gotRoundTrip)
})
}
}

View File

@@ -1,307 +0,0 @@
package genaimessages
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// convertSemconvMessages handles [{role, parts, finish_reason}] and Gemini contents.
func convertSemconvMessages(value any) ([]aiobservabilitytypes.Message, bool) {
list, ok := value.([]any)
if !ok || len(list) == 0 {
return nil, false
}
first, ok := toObject(firstItem(list))
if !ok {
return nil, false
}
if ok := first.has("parts"); !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(list))
for _, item := range list {
m, ok := toObject(item)
if !ok {
messages = append(messages, genericMessages(stringOf(item))...)
continue
}
messages = append(messages, semconvMessage(m, "")...)
}
return messages, true
}
// convertChatMessageList handles OpenAI, Anthropic, Vercel and LangChain message lists.
func convertChatMessageList(value any) ([]aiobservabilitytypes.Message, bool) {
list, ok := value.([]any)
if !ok || len(list) == 0 {
return nil, false
}
if !isChatMessage(firstItem(list)) {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(list))
for _, item := range list {
messages = append(messages, chatMessage(item, "")...)
}
return messages, true
}
func isChatMessage(value any) bool {
m, ok := toObject(value)
if !ok {
return false
}
if ok := m.has("role"); ok {
return true
}
switch typ := m.str("type"); typ {
case "message", "reasoning", "human", "ai", "tool", "system":
return true
case "constructor":
ok := m.has("kwargs")
return ok
default:
return isResponsesItemType(typ)
}
}
// convertToolCallList handles a bare tool call list, e.g. Vercel ai.response.toolCalls.
func convertToolCallList(value any) ([]aiobservabilitytypes.Message, bool) {
list, ok := value.([]any)
if !ok || len(list) == 0 {
return nil, false
}
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleAssistant, Content: make([]aiobservabilitytypes.Part, 0, len(list))}
for _, item := range list {
call, ok := toObject(item)
if !ok || !isToolCall(call) {
return nil, false
}
msg.Content = append(msg.Content, toolCallPart(call))
}
return []aiobservabilitytypes.Message{msg}, true
}
// isToolCall rejects tool definitions, which carry no arguments.
func isToolCall(call object) bool {
if has := call.has("toolName"); has {
return true
}
if fn, ok := call.obj("function"); ok {
has := fn.has("arguments")
return has
}
if has := call.has("name"); !has {
return false
}
_, has := call.lookup("arguments", "args")
return has
}
// convertChatRequest handles OpenAI, Anthropic, Vercel, Gemini and LangChain request objects.
func convertChatRequest(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
conversation, ok := m.lookup("messages", "input", "contents", "prompt")
if !ok {
return nil, false
}
messages := []aiobservabilitytypes.Message{}
if system := systemMessage(m.first("system", "instructions", "system_instruction", "systemInstruction", "system_prompt")); system != nil {
messages = append(messages, *system)
} else if config, ok := m.obj("config"); ok {
if system := systemMessage(config.first("system_instruction", "systemInstruction")); system != nil {
messages = append(messages, *system)
}
}
// {messages: "[...]"}: the list arrives JSON-encoded once more from some SDKs
if s, isString := conversation.(string); isString {
var decoded any
if err := json.Unmarshal([]byte(s), &decoded); err == nil {
if _, isList := decoded.([]any); isList {
conversation = decoded
}
}
}
switch c := conversation.(type) {
case string:
if c != "" {
messages = append(messages, textMessage(aiobservabilitytypes.MessageRoleUser, c))
}
case []any:
for _, item := range flattenOnce(c) {
if s, isString := item.(string); isString {
messages = append(messages, textMessage(aiobservabilitytypes.MessageRoleUser, s))
continue
}
messages = append(messages, semconvMessage(asObject(item), aiobservabilitytypes.MessageRoleUser)...)
}
case map[string]any:
messages = append(messages, semconvMessage(c, aiobservabilitytypes.MessageRoleUser)...)
default:
return nil, false
}
return messages, true
}
// systemMessage returns nil when value is empty or unknown.
func systemMessage(value any) *aiobservabilitytypes.Message {
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleSystem, Content: []aiobservabilitytypes.Part{}}
switch v := value.(type) {
case string:
if v == "" {
return nil
}
msg.Content = append(msg.Content, textPart(v))
case []any:
for _, item := range v {
msg.Content = append(msg.Content, contentBlockPart(item))
}
case map[string]any:
parts, ok := object(v).list("parts")
if !ok {
return nil
}
for _, p := range parts {
msg.Content = append(msg.Content, semconvPart(p))
}
default:
return nil
}
if len(msg.Content) == 0 {
return nil
}
return &msg
}
// convertChatResponse handles the OpenAI Chat Completions {choices} response.
func convertChatResponse(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
choices, ok := m.list("choices")
if !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(choices))
for _, c := range choices {
choice := asObject(c)
var converted []aiobservabilitytypes.Message
if message, ok := toObject(choice.first("message", "delta")); ok {
converted = chatMessage(message, aiobservabilitytypes.MessageRoleAssistant)
} else if text, ok := choice.get("text"); ok {
converted = []aiobservabilitytypes.Message{textMessage(aiobservabilitytypes.MessageRoleAssistant, stringOf(text))}
} else {
converted = []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{genericPart(choice)}}}
}
messages = append(messages, withFinishReason(converted, choice.str("finish_reason"))...)
}
return messages, true
}
func convertResponsesAPIResponse(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
output, ok := m.list("output")
if !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(output))
for _, item := range output {
messages = append(messages, chatMessage(item, aiobservabilitytypes.MessageRoleAssistant)...)
}
if last := lastMessage(messages); last != nil && last.FinishReason == "" {
if details, ok := m.obj("incomplete_details"); ok {
last.FinishReason = normalizeFinishReason(details.str("reason"))
} else if m.str("status") == "completed" {
last.FinishReason = aiobservabilitytypes.FinishReasonStop
}
}
return messages, true
}
// convertGeminiResponse handles Gemini {candidates} and Google ADK {content} responses.
func convertGeminiResponse(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
if content, ok := m.obj("content"); ok {
if hasParts := content.has("parts"); hasParts {
return withFinishReason(semconvMessage(content, aiobservabilitytypes.MessageRoleAssistant), finishReasonOf(m)), true
}
}
candidates, ok := m.list("candidates")
if !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(candidates))
for _, c := range candidates {
candidate := asObject(c)
content, ok := candidate.obj("content")
if !ok {
generic := []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{genericPart(candidate)}}}
messages = append(messages, withFinishReason(generic, finishReasonOf(candidate))...)
continue
}
messages = append(messages, withFinishReason(semconvMessage(content, aiobservabilitytypes.MessageRoleAssistant), finishReasonOf(candidate))...)
}
return messages, true
}
func convertLegacyCompletion(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
completion, ok := m.text("completion")
if !ok {
return nil, false
}
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{}}
if reasoning, ok := m.text("reasoning"); ok && reasoning != "" {
msg.Content = append(msg.Content, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: reasoning})
}
msg.Content = append(msg.Content, textPart(completion))
return []aiobservabilitytypes.Message{msg}, true
}
func convertLangChainGenerations(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
generations, ok := m.list("generations")
if !ok {
return nil, false
}
messages := []aiobservabilitytypes.Message{}
for _, g := range flattenOnce(generations) {
gen := asObject(g)
var converted []aiobservabilitytypes.Message
if message, ok := gen.obj("message"); ok {
converted = chatMessage(message, aiobservabilitytypes.MessageRoleAssistant)
} else {
converted = []aiobservabilitytypes.Message{textMessage(aiobservabilitytypes.MessageRoleAssistant, gen.str("text"))}
}
messages = append(messages, withFinishReason(converted, asObject(gen.at("generation_info")).str("finish_reason"))...)
}
return messages, true
}
// withFinishReason sets reason on the last message that has none.
func withFinishReason(messages []aiobservabilitytypes.Message, reason string) []aiobservabilitytypes.Message {
if last := lastMessage(messages); last != nil && last.FinishReason == "" {
last.FinishReason = normalizeFinishReason(reason)
}
return messages
}

View File

@@ -1,288 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func TestNormalizeEnvelopeRequests(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "AnthropicRequest_SystemStringBecomesSystemMessage",
raw: `{
"model": "claude",
"system": "Be brief.",
"messages": [{"role": "user", "content": "Hi"}],
"max_tokens": 100
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleSystem,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Be brief."},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
{
name: "GeminiRequest_ConfigSystemInstructionBecomesSystemMessage",
raw: `{
"model": "gemini-2.0",
"config": {"system_instruction": "Be brief."},
"contents": [
{"role": "user", "parts": [{"text": "Hi"}]},
{
"role": "user",
"parts": [
{
"function_response": {"name": "get_weather", "response": {"temp": 20}}
}
]
}
]
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleSystem,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Be brief."},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, Name: "get_weather", Content: `{"temp":20}`},
},
},
},
},
{
name: "GeminiRequest_SystemInstructionPartsBecomeSystemMessage",
raw: `{"system_instruction": {"parts": [{"text": "Be brief."}]}, "contents": [{"role": "user", "parts": [{"text": "Hi"}]}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleSystem,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Be brief."},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
{
name: "RequestWithMessagesAsJSONString_MessagesDecoded",
raw: `{"messages":"[{\"role\":\"user\",\"content\":\"Hi\"}]"}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
})
}
func TestNormalizeEnvelopeChatResponses(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "StreamingChunkDelta_BecomesAssistantText",
raw: `{"choices": [{"delta": {"role": "assistant", "content": "Hel"}, "finish_reason": null}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hel"},
},
},
},
},
{
name: "LegacyCompletionChoiceText_BecomesAssistantTextWithLength",
raw: `{"choices": [{"text": "Hello", "finish_reason": "length"}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonLength,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hello"},
},
},
},
},
{
name: "FilteredChoiceWithoutMessage_BecomesGenericWithContentFilter",
raw: `{"choices": [{"index": 0, "finish_reason": "content_filter"}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonContentFilter,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"finish_reason":"content_filter","index":0}`},
},
},
},
},
})
}
func TestNormalizeEnvelopeOpenAIResponses(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "ResponsesAPIResponse_MessageAndReasoningItemsConverted",
raw: `{
"object": "response",
"status": "completed",
"output": [
{
"type": "reasoning",
"id": "rs_1",
"summary": [{"type": "summary_text", "text": "Thinking about it"}]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Paris."}]
}
]
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Thinking about it"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Paris."},
},
FinishReason: aiobservabilitytypes.FinishReasonStop,
},
},
},
{
name: "ResponsesAPIResponseIncomplete_EncryptedReasoningEmptyAndFinishReasonLength",
raw: `{
"output": [
{"type": "reasoning", "content": [{"type": "reasoning_text", "text": "step 1"}]},
{"type": "reasoning", "encrypted_content": "gAAAA"}
],
"status": "incomplete",
"incomplete_details": {"reason": "max_output_tokens"}
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "step 1"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonLength,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking},
},
},
},
},
})
}
func TestNormalizeEnvelopeGemini(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "GeminiResponse_ThoughtAndFunctionCallPartsConverted",
raw: `{
"candidates": [
{
"content": {
"parts": [
{"text": "Let me check", "thought": true},
{"function_call": {"name": "get_weather", "args": {"city": "Paris"}}}
],
"role": "model"
},
"finishReason": "STOP"
}
],
"usageMetadata": {}
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonStop,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Let me check"},
{Type: aiobservabilitytypes.PartTypeToolCall, Name: "get_weather", Arguments: map[string]any{"city": "Paris"}},
},
},
},
},
{
name: "GoogleADKResponse_ContentBecomesAssistantMessageWithStop",
raw: `{"content": {"parts": [{"text": "It is 18°C and clear in Bengaluru."}], "role": "model"}, "finish_reason": "STOP"}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonStop,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "It is 18°C and clear in Bengaluru."},
},
},
},
},
{
name: "GeminiResponseBlockedBySafety_BecomesGenericWithContentFilter",
raw: `{"candidates": [{"finishReason": "SAFETY", "index": 0}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonContentFilter,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"finishReason":"SAFETY","index":0}`},
},
},
},
},
})
}
func TestNormalizeEnvelopeLangChain(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "LangChainGenerationWithOnlyText_BecomesAssistantTextWithLength",
raw: `{"generations": [[{"text": "Observability is the ability", "generation_info": {"finish_reason": "length"}}]]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonLength,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Observability is the ability"},
},
},
},
},
})
}

View File

@@ -1,71 +0,0 @@
package genaimessages
import (
"encoding/json"
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// Ordered by specificity: earlier converters never match a later format.
var converters = []converter{
convertSemconvMessages,
convertChatMessageList,
convertToolCallList,
convertChatRequest,
convertChatResponse,
convertResponsesAPIResponse,
convertGeminiResponse,
convertLegacyCompletion,
convertLangChainGenerations,
}
// Normalize converts a gen_ai.*.messages value, a JSON string or a decoded value.
// A string that is not JSON becomes one text part; JSON in no known format becomes
// one generic part holding the original.
func Normalize(raw any) []aiobservabilitytypes.Message {
var (
value any
original string
)
switch v := raw.(type) {
case nil:
return []aiobservabilitytypes.Message{}
case string:
if strings.TrimSpace(v) == "" {
return []aiobservabilitytypes.Message{}
}
original = v
if err := json.Unmarshal([]byte(v), &value); err != nil {
return []aiobservabilitytypes.Message{{Content: []aiobservabilitytypes.Part{textPart(v)}}}
}
default:
value = v
original = stringOf(v)
}
if value == nil {
return []aiobservabilitytypes.Message{}
}
if list, ok := value.([]any); ok {
if len(list) == 0 {
return []aiobservabilitytypes.Message{}
}
// [[...]]: some SDKs wrap the conversation in one more list
if _, nested := firstItem(list).([]any); nested {
value = flattenOnce(list)
}
// ["{...}", "{...}"]: an array attribute holding one JSON message per element
if decoded, ok := decodeJSONStrings(list); ok {
value = decoded
}
}
for _, convert := range converters {
if messages, ok := convert(value); ok {
return messages
}
}
return genericMessages(original)
}
type converter func(value any) (messages []aiobservabilitytypes.Message, ok bool)

View File

@@ -1,146 +0,0 @@
package genaimessages
import (
"encoding/json"
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
)
func TestNormalize(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "ListNestedInAnotherList_Unwrapped",
raw: `[[{"role":"user","content":"Hi"}]]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
{
name: "ListOfToolDefinitions_FallsBackToGeneric",
raw: `[{"type":"function","function":{"name":"get_weather","description":"Weather","parameters":{"type":"object"}}}]`,
want: []aiobservabilitytypes.Message{
{
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `[{"type":"function","function":{"name":"get_weather","description":"Weather","parameters":{"type":"object"}}}]`},
},
},
},
},
{
name: "ListOfMessagesAsJSONStrings_EachDecoded",
raw: []any{`{"role":"user","content":"Hi"}`, `{"role":"assistant","content":"Hello"}`},
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hello"},
},
},
},
},
{
name: "TextThatIsNotJSON_ReturnedAsOneTextPart",
raw: "Let the cost of the ball be x dollars.",
want: []aiobservabilitytypes.Message{
{
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Let the cost of the ball be x dollars."},
},
},
},
},
{
name: "JSONInNoKnownFormat_ReturnedVerbatimAsGeneric",
raw: `{"output": "{\"query\": \"SigNoz\"}", "kwargs": {"name": "search_web"}}`,
want: []aiobservabilitytypes.Message{
{
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"output": "{\"query\": \"SigNoz\"}", "kwargs": {"name": "search_web"}}`},
},
},
},
},
{
name: "AlreadyDecodedValue_ConvertedLikeJSON",
raw: []any{map[string]any{"role": "user", "content": "hi"}},
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "hi"},
},
},
},
},
{
name: "BlankString_ReturnsNoMessages",
raw: " \n\t ",
want: []aiobservabilitytypes.Message{},
},
{
name: "JSONNull_ReturnsNoMessages",
raw: `null`,
want: []aiobservabilitytypes.Message{},
},
})
}
func FuzzNormalize(f *testing.F) {
for _, seed := range []string{
`[{"role": "user", "parts": [{"type": "text", "content": "Hi"}]}]`,
`[{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_1", "name": "f", "input": {}}]}]`,
`[{"role": "tool", "tool_call_id": "call_1", "content": [{"type": "text", "text": "ok"}]}]`,
`{"messages": [{"role": "user", "content": "Hi"}], "system": [{"type": "text", "text": "Be brief."}]}`,
`{"choices": [{"message": {"role": "assistant", "content": "Hi"}, "finish_reason": "stop"}]}`,
`{"output": [{"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"}, {"type": "reasoning"}]}`,
`{"candidates": [{"content": {"parts": [{"functionCall": {"name": "f", "args": {}}}], "role": "model"}}]}`,
`{"generations": [[{"text": "Hi", "message": {"lc": 1, "type": "constructor", "id": ["AIMessage"], "kwargs": {}}}]]}`,
`[[{"role": "user", "content": "Hi"}]]`,
`["{\"role\": \"user\", \"content\": \"Hi\"}"]`,
``, `null`, `{}`, `[]`, `[{}]`, `"text"`, `42`,
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, raw string) {
inputs := []any{raw}
var decoded any
if json.Unmarshal([]byte(raw), &decoded) == nil {
inputs = append(inputs, decoded)
}
for _, input := range inputs {
for i, message := range Normalize(input) {
if message.Content == nil {
t.Fatalf("message %d has nil content for %#v", i, input)
}
}
}
})
}
type normalizeCase struct {
name string
raw any
want []aiobservabilitytypes.Message
}
func assertNormalize(t *testing.T, testCases []normalizeCase) {
t.Helper()
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, Normalize(testCase.raw))
})
}
}

View File

@@ -1,254 +0,0 @@
package genaimessages
import (
"cmp"
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// chatMessage returns nil for LangGraph tool definitions.
func chatMessage(value any, defaultRole aiobservabilitytypes.MessageRole) []aiobservabilitytypes.Message {
m, ok := toObject(value)
if !ok {
return genericMessages(stringOf(value))
}
if inner, role, ok := langChainMessage(m); ok {
return chatMessage(inner, role)
}
if isLangGraphToolDefinition(m) {
return nil
}
typ := m.str("type")
if isResponsesItemType(typ) {
return responsesItemMessages(m, typ)
}
if typ == "reasoning" {
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: reasoningParts(m)}}
}
role := cmp.Or(normalizeRole(m.str("role")), standardRole(typ), defaultRole)
msg := aiobservabilitytypes.Message{
Role: role,
Content: append(messageContentParts(m, role), messageToolCallParts(m)...),
FinishReason: normalizeFinishReason(finishReasonOf(m)),
}
if refusal := m.str("refusal"); refusal != "" {
msg.Content = append(msg.Content, textPart(refusal))
}
return []aiobservabilitytypes.Message{msg}
}
// langChainMessage unwraps a serialised message: {lc, type: "constructor", id: [..., "HumanMessage"], kwargs}.
func langChainMessage(m object) (object, aiobservabilitytypes.MessageRole, bool) {
kwargs, ok := m.obj("kwargs")
if !ok || m.str("type") != "constructor" {
return nil, "", false
}
return kwargs, cmp.Or(langChainRole(m.at("id")), standardRole(kwargs.str("type"))), true
}
func langChainRole(id any) aiobservabilitytypes.MessageRole {
path, ok := id.([]any)
if !ok || len(path) == 0 {
return ""
}
switch class := stringOf(lastItem(path)); {
case strings.HasPrefix(class, "System"):
return aiobservabilitytypes.MessageRoleSystem
case strings.HasPrefix(class, "Human"):
return aiobservabilitytypes.MessageRoleUser
case strings.HasPrefix(class, "AI"):
return aiobservabilitytypes.MessageRoleAssistant
case strings.HasPrefix(class, "Tool"), strings.HasPrefix(class, "Function"):
return aiobservabilitytypes.MessageRoleTool
}
return ""
}
// isLangGraphToolDefinition matches {role: "tool", content: {type: "function"}} without tool_call_id.
func isLangGraphToolDefinition(m object) bool {
if normalizeRole(m.str("role")) != aiobservabilitytypes.MessageRoleTool {
return false
}
if has := m.has("tool_call_id"); has {
return false
}
content, ok := m.obj("content")
if !ok || content.str("type") != "function" {
return false
}
ok = content.has("function")
return ok
}
// messageContentParts turns a tool message's text into its result.
func messageContentParts(m object, role aiobservabilitytypes.MessageRole) []aiobservabilitytypes.Part {
parts := []aiobservabilitytypes.Part{}
switch content := m.at("content").(type) {
case nil:
case string:
if role == aiobservabilitytypes.MessageRoleTool {
parts = append(parts, toolMessageResult(m, content))
} else if content != "" {
parts = append(parts, textPart(content))
}
case []any:
for _, item := range content {
part := contentBlockPart(item)
if role == aiobservabilitytypes.MessageRoleTool && part.Type == aiobservabilitytypes.PartTypeText {
part = toolMessageResult(m, part.Content)
}
parts = append(parts, part)
}
case map[string]any:
if contentParts, ok := object(content).list("parts"); ok {
for _, p := range contentParts {
parts = append(parts, semconvPart(p))
}
} else if role == aiobservabilitytypes.MessageRoleTool {
parts = append(parts, toolMessageResult(m, stringOf(content)))
} else {
parts = append(parts, genericPart(content))
}
default:
parts = append(parts, genericPart(content))
}
return parts
}
func messageToolCallParts(m object) []aiobservabilitytypes.Part {
calls, _ := m.first("tool_calls", "toolCalls").([]any)
if kwargs, ok := m.obj("additional_kwargs"); ok && len(calls) == 0 {
calls, _ = kwargs.list("tool_calls")
}
parts := make([]aiobservabilitytypes.Part, 0, len(calls)+1)
for _, call := range calls {
parts = append(parts, toolCallPart(call))
}
if call, ok := m.obj("function_call"); ok {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolCall, Name: call.str("name"), Arguments: parseArguments(call.at("arguments"))})
}
return parts
}
func toolMessageResult(m object, content string) aiobservabilitytypes.Part {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: stringOf(m.first("tool_call_id", "toolCallId")), Name: m.str("name"), Content: content}
}
// semconvMessage falls back to chatMessage when m has no parts. LiteLLM mixes in
// OpenAI fields: tool_calls beside parts, and tool_call_id on tool messages.
func semconvMessage(m object, defaultRole aiobservabilitytypes.MessageRole) []aiobservabilitytypes.Message {
parts, ok := m.list("parts")
if !ok {
return chatMessage(m, defaultRole)
}
role := cmp.Or(normalizeRole(m.str("role")), defaultRole)
msg := aiobservabilitytypes.Message{
Role: role,
Content: []aiobservabilitytypes.Part{},
FinishReason: normalizeFinishReason(finishReasonOf(m)),
}
for _, p := range parts {
part := semconvPart(p)
if role == aiobservabilitytypes.MessageRoleTool && part.Type == aiobservabilitytypes.PartTypeText {
part = toolMessageResult(m, part.Content)
}
msg.Content = append(msg.Content, part)
}
msg.Content = append(msg.Content, messageToolCallParts(m)...)
return []aiobservabilitytypes.Message{msg}
}
// isResponsesItemType matches role-less Responses API tool and MCP items.
func isResponsesItemType(typ string) bool {
switch typ {
case "":
return false
case "function_call", "function_call_output", "tool_call", "custom_tool_call", "custom_tool_call_output",
"mcp_call", "mcp_list_tools", "mcp_approval_request", "mcp_approval_response":
return true
}
return strings.HasSuffix(typ, "_call") || strings.HasSuffix(typ, "_call_output")
}
// responsesItemMessages turns a built-in tool item into a call, plus its result when the item carries one.
func responsesItemMessages(m object, typ string) []aiobservabilitytypes.Message {
switch typ {
case "function_call", "tool_call", "custom_tool_call":
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{{
Type: aiobservabilitytypes.PartTypeToolCall,
ID: stringOf(m.first("call_id", "id")),
Name: m.str("name"),
Arguments: parseArguments(m.first("arguments", "args", "input")),
}}}}
case "function_call_output", "custom_tool_call_output":
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleTool, Content: []aiobservabilitytypes.Part{{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: stringOf(m.first("call_id", "id")),
Content: stringOf(m.first("output", "result")),
}}}}
}
id := stringOf(m.first("call_id", "id"))
if strings.HasSuffix(typ, "_output") || typ == "mcp_approval_response" {
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleTool, Content: []aiobservabilitytypes.Part{{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: id,
Name: strings.TrimSuffix(typ, "_output"),
Content: stringOf(m.first("output", "result", "results")),
}}}}
}
args := make(map[string]any, len(m))
for k, v := range m {
switch k {
case "type", "id", "call_id", "status", "name", "server_label", "output", "result", "results":
default:
args[k] = v
}
}
name := stringOf(m.first("name", "server_label"))
if name == "" {
name = typ
}
call := aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolCall, ID: id, Name: name}
if arguments, ok := m.get("arguments"); ok {
call.Arguments = parseArguments(arguments)
} else if len(args) > 0 {
call.Arguments = args
}
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{call}}
if result := m.first("output", "result", "results"); result != nil {
msg.Content = append(msg.Content, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: id, Name: name, Content: stringOf(result)})
}
return []aiobservabilitytypes.Message{msg}
}
// reasoningParts returns one empty thinking part for encrypted reasoning without a summary.
func reasoningParts(m object) []aiobservabilitytypes.Part {
parts := []aiobservabilitytypes.Part{}
if summary, ok := m.list("summary"); ok {
for _, s := range summary {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(asObject(s).first("text", "content"))})
}
}
if content, ok := m.list("content"); ok {
for _, c := range content {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(asObject(c).first("text", "content"))})
}
}
if len(parts) == 0 {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking})
}
return parts
}
func textMessage(role aiobservabilitytypes.MessageRole, content string) aiobservabilitytypes.Message {
return aiobservabilitytypes.Message{Role: role, Content: []aiobservabilitytypes.Part{textPart(content)}}
}
func genericMessages(content string) []aiobservabilitytypes.Message {
return []aiobservabilitytypes.Message{{Content: []aiobservabilitytypes.Part{genericPart(content)}}}
}

View File

@@ -1,257 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func TestNormalizeMessageChat(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "AnthropicMessages_ThinkingAndToolUseBlocksConverted",
raw: `[
{"role": "user", "content": "Hi"},
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "Let me see"},
{"type": "redacted_thinking", "data": "x"},
{
"type": "tool_use",
"id": "toolu_1",
"name": "lookup",
"input": {"q": "a"}
}
],
"stop_reason": "tool_use"
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "found",
"is_error": true
}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonToolCall,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Let me see"},
{Type: aiobservabilitytypes.PartTypeThinking},
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "toolu_1", Name: "lookup", Arguments: map[string]any{"q": "a"}},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "toolu_1", Content: "found", IsError: true},
},
},
},
},
{
name: "OpenAIToolMessageTextBlocks_BecomeToolResult",
raw: `[
{
"role": "tool",
"tool_call_id": "c1",
"content": [{"type": "text", "text": "20C"}, {"type": "text", "text": "clear"}]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "c1", Content: "20C"},
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "c1", Content: "clear"},
},
},
},
},
{
name: "OpenAIRefusal_BecomesAssistantText",
raw: `[{"role": "assistant", "content": null, "refusal": "I can't help with that."}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "I can't help with that."},
},
},
},
},
{
name: "OpenAILegacyFunctionCall_BecomesToolCall",
raw: `[{"role": "assistant", "content": null, "function_call": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, Name: "get_weather", Arguments: map[string]any{"city": "Paris"}},
},
},
},
},
})
}
func TestNormalizeMessageLangChain(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "LangChainTypedMessages_ToolCallsReadFromAdditionalKwargs",
raw: `[
{"type": "human", "content": "Weather?"},
{
"type": "ai",
"content": "",
"additional_kwargs": {
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}
}
]
}
},
{
"type": "tool",
"content": "20C",
"tool_call_id": "call_1",
"name": "get_weather"
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Weather?"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "call_1", Name: "get_weather", Arguments: map[string]any{"city": "Paris"}},
},
},
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "call_1", Name: "get_weather", Content: "20C"},
},
},
},
},
{
name: "LangGraphToolDefinitionMessage_IsDropped",
raw: `[
{
"role": "tool",
"content": {
"type": "function",
"function": {"name": "get_weather", "parameters": {}}
}
},
{"role": "user", "content": "Hi"}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
})
}
func TestNormalizeMessageOpenAIResponsesItems(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "BuiltInToolCallItem_BecomesToolCall",
raw: `{
"object": "response",
"status": "completed",
"output": [
{
"type": "web_search_call",
"id": "ws_1",
"status": "completed",
"action": {"type": "search", "query": "SigNoz"}
},
{
"type": "custom_tool_call",
"call_id": "c1",
"name": "grep",
"input": "foo"
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Found it."}]
}
]
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "ws_1", Name: "web_search_call", Arguments: map[string]any{"action": map[string]any{"type": "search", "query": "SigNoz"}}},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "c1", Name: "grep", Arguments: "foo"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Found it."},
},
FinishReason: aiobservabilitytypes.FinishReasonStop,
},
},
},
{
name: "BuiltInToolOutputItem_BecomesToolResult",
raw: `[{"type": "local_shell_call_output", "call_id": "call_2", "output": "ok"}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "call_2", Name: "local_shell_call", Content: "ok"},
},
},
},
},
{
name: "MCPCallItemWithOutput_BecomesToolCallAndToolResult",
raw: `[{"type": "mcp_call", "id": "mcp_1", "server_label": "deepwiki", "name": "ask_question", "arguments": "{\"q\": \"What is SigNoz?\"}", "output": "An observability platform."}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "mcp_1", Name: "ask_question", Arguments: map[string]any{"q": "What is SigNoz?"}},
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "mcp_1", Name: "ask_question", Content: "An observability platform."},
},
},
},
},
})
}

View File

@@ -1,152 +0,0 @@
package genaimessages
import (
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func contentBlockPart(value any) aiobservabilitytypes.Part {
p, ok := toObject(value)
if !ok {
if s, ok := value.(string); ok {
return textPart(s)
}
return genericPart(value)
}
typ := p.str("type")
switch typ {
case "text", "input_text", "output_text", "refusal", "summary_text":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: stringOf(p.first("text", "content", "refusal"))}
case "thinking", "reasoning":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(p.first("thinking", "text", "content", "reasoning"))}
case "redacted_thinking":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking}
case "tool-call", "tool_use", "tool_call", "function_call", "server_tool_use", "mcp_tool_use":
return toolCallPart(p)
case "tool-result", "tool_result", "function_call_output":
return toolResultBlockPart(p, p.first("result", "output", "content"))
case "":
if text, ok := p.get("text"); ok {
return textPart(stringOf(text))
}
default:
if strings.HasSuffix(typ, "_tool_result") {
return toolResultBlockPart(p, p.at("content"))
}
}
return genericPart(p)
}
func semconvPart(value any) aiobservabilitytypes.Part {
p, ok := toObject(value)
if !ok {
if s, ok := value.(string); ok {
return textPart(s)
}
return genericPart(value)
}
switch p.str("type") {
case "text":
if p.flag("thought") {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(p.first("content", "text"))}
}
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: stringOf(p.first("content", "text"))}
case "reasoning", "thinking":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(p.first("content", "thinking", "text"))}
case "redacted_thinking", "redacted_reasoning":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking}
case "tool_call":
return aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolCall,
ID: p.str("id"),
Name: p.str("name"),
Arguments: parseArguments(p.first("arguments", "args", "input")),
}
case "tool_call_response":
return aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: p.str("id"),
Name: p.str("name"),
Content: stringOf(p.first("response", "result", "content", "output")),
IsError: boolOf(p.first("is_error", "isError")),
}
case "":
// Gemini parts carry no type; the field name is the type.
if text, ok := p.get("text"); ok {
if p.flag("thought") {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(text)}
}
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: stringOf(text)}
}
if call, ok := toObject(p.first("functionCall", "function_call")); ok {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolCall, ID: call.str("id"), Name: call.str("name"), Arguments: parseArguments(call.at("args"))}
}
if resp, ok := toObject(p.first("functionResponse", "function_response")); ok {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: resp.str("id"), Name: resp.str("name"), Content: resp.str("response")}
}
}
return genericPart(p)
}
// toolCallPart reads the OpenAI, flat, Anthropic and Vercel tool call shapes.
func toolCallPart(value any) aiobservabilitytypes.Part {
call, ok := toObject(value)
if !ok {
return genericPart(value)
}
part := aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolCall,
ID: stringOf(call.first("toolCallId", "call_id", "id")),
Name: stringOf(call.first("toolName", "name")),
}
if fn, ok := call.obj("function"); ok {
part.Name = fn.str("name")
part.Arguments = parseArguments(fn.at("arguments"))
return part
}
part.Arguments = parseArguments(call.first("arguments", "args", "input"))
return part
}
// toolResultBlockPart unwraps the Vercel {type, value} result wrapper.
func toolResultBlockPart(p object, result any) aiobservabilitytypes.Part {
if nested, ok := toObject(result); ok && len(nested) <= 2 {
if v, ok := nested.get("value"); ok {
result = v
}
}
return aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: stringOf(p.first("toolCallId", "tool_use_id", "tool_call_id", "call_id", "id")),
Name: stringOf(p.first("toolName", "name")),
Content: toolResultText(result),
IsError: boolOf(p.first("isError", "is_error")),
}
}
// toolResultText joins a list of text blocks; anything else goes through stringOf.
func toolResultText(result any) string {
list, ok := result.([]any)
if !ok || len(list) == 0 {
return stringOf(result)
}
texts := make([]string, 0, len(list))
for _, item := range list {
block := asObject(item)
text, ok := block.text("text")
if !ok || (len(block) == 2 && block.str("type") != "text") || len(block) > 2 {
return stringOf(result)
}
texts = append(texts, text)
}
return strings.Join(texts, "\n")
}
func textPart(content string) aiobservabilitytypes.Part {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: content}
}
func genericPart(value any) aiobservabilitytypes.Part {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeGeneric, Content: stringOf(value)}
}

View File

@@ -1,130 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func TestNormalizePartContent(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "PartWithUnknownType_BecomesGeneric",
raw: `[{"role":"user","parts":[{"type":"image","url":"http://x/y.png"}]}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"type":"image","url":"http://x/y.png"}`},
},
},
},
},
})
}
func TestNormalizePartThinking(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "ThoughtReasoningAndHiddenParts_BecomeThinking",
raw: `[
{
"role": "assistant",
"parts": [
{"type": "text", "content": "Let me think", "thought": true},
{"type": "reasoning", "content": "step 1"},
{"type": "redacted_reasoning", "data": "xyz"},
{"type": "text", "content": "Answer"}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Let me think"},
{Type: aiobservabilitytypes.PartTypeThinking, Content: "step 1"},
{Type: aiobservabilitytypes.PartTypeThinking},
{Type: aiobservabilitytypes.PartTypeText, Content: "Answer"},
},
},
},
},
})
}
func TestNormalizePartTools(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "AnthropicServerToolUseAndWebSearchResult_BecomeToolCallAndResult",
raw: `[
{
"role": "assistant",
"content": [
{"type": "server_tool_use", "id": "srvtoolu_1", "name": "web_search", "input": {"query": "SigNoz"}},
{"type": "web_search_tool_result", "tool_use_id": "srvtoolu_1", "content": [{"type": "web_search_result", "title": "SigNoz", "url": "https://signoz.io"}]}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "srvtoolu_1", Name: "web_search", Arguments: map[string]any{"query": "SigNoz"}},
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "srvtoolu_1", Content: `[{"title":"SigNoz","type":"web_search_result","url":"https://signoz.io"}]`},
},
},
},
},
{
name: "AnthropicToolResultTextBlocks_JoinedIntoOneString",
raw: `[
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "t1",
"content": [
{"type": "text", "text": "line one"},
{"type": "text", "text": "line two"}
]
}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "t1", Content: "line one\nline two"},
},
},
},
},
{
name: "VercelToolResultJSONWrapper_ValueUnwrapped",
raw: `[{"role": "tool", "content": [{"type": "tool-result", "toolCallId": "call_1", "toolName": "get_weather", "result": {"type": "json", "value": {"temp_c": 18}}}]}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "call_1", Name: "get_weather", Content: `{"temp_c":18}`},
},
},
},
},
{
name: "ToolCallWithInvalidJSONArguments_ArgumentsKeptAsString",
raw: `[{"role": "assistant", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{city: Paris"}}]}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "call_1", Name: "get_weather", Arguments: "{city: Paris"},
},
},
},
},
})
}

View File

@@ -1,54 +0,0 @@
package genaimessages
import (
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
var finishReasonKeys = []string{"finish_reason", "finishReason", "stop_reason"}
// normalizeRole keeps an unknown role, lowercased.
func normalizeRole(role string) aiobservabilitytypes.MessageRole {
if known := standardRole(role); known != "" {
return known
}
return aiobservabilitytypes.MessageRole(strings.ToLower(strings.TrimSpace(role)))
}
// normalizeFinishReason keeps an unknown reason, lowercased.
func normalizeFinishReason(reason string) aiobservabilitytypes.FinishReason {
lowered := strings.ToLower(strings.TrimSpace(reason))
switch lowered {
case "stop", "end_turn", "stop_sequence", "completed", "complete", "eos", "finished":
return aiobservabilitytypes.FinishReasonStop
case "tool_call", "tool_calls", "tool_use", "function_call":
return aiobservabilitytypes.FinishReasonToolCall
case "length", "max_tokens", "max_output_tokens", "max_completion_tokens", "model_length":
return aiobservabilitytypes.FinishReasonLength
case "content_filter", "content_filtered", "guardrail_intervened", "safety", "refusal", "recitation", "blocklist", "prohibited_content", "spii":
return aiobservabilitytypes.FinishReasonContentFilter
case "error", "failed", "incomplete":
return aiobservabilitytypes.FinishReasonError
}
return aiobservabilitytypes.FinishReason(lowered)
}
// standardRole maps vendor role names onto MessageRole; anything else is "".
func standardRole(role string) aiobservabilitytypes.MessageRole {
switch strings.ToLower(strings.TrimSpace(role)) {
case "system", "developer":
return aiobservabilitytypes.MessageRoleSystem
case "user", "human":
return aiobservabilitytypes.MessageRoleUser
case "assistant", "ai", "model":
return aiobservabilitytypes.MessageRoleAssistant
case "tool", "function":
return aiobservabilitytypes.MessageRoleTool
}
return ""
}
func finishReasonOf(m object) string {
return stringOf(m.first(finishReasonKeys...))
}

View File

@@ -1,53 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
)
func TestNormalizeRole(t *testing.T) {
testCases := []struct {
name string
roles []string
want aiobservabilitytypes.MessageRole
}{
{name: "SystemAndDeveloper_MapToSystem", roles: []string{"system", "developer", " Developer "}, want: aiobservabilitytypes.MessageRoleSystem},
{name: "UserAndHuman_MapToUser", roles: []string{"user", "human", "HUMAN"}, want: aiobservabilitytypes.MessageRoleUser},
{name: "AssistantAIAndModel_MapToAssistant", roles: []string{"assistant", "ai", "model"}, want: aiobservabilitytypes.MessageRoleAssistant},
{name: "ToolAndFunction_MapToTool", roles: []string{"tool", "function"}, want: aiobservabilitytypes.MessageRoleTool},
{name: "UnknownRole_KeptLowercased", roles: []string{"Narrator"}, want: "narrator"},
{name: "EmptyRole_StaysEmpty", roles: []string{"", " "}, want: ""},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
for _, role := range testCase.roles {
assert.Equal(t, testCase.want, normalizeRole(role), role)
}
})
}
}
func TestNormalizeFinishReason(t *testing.T) {
testCases := []struct {
name string
reasons []string
want aiobservabilitytypes.FinishReason
}{
{name: "EndOfAnswer_MapsToStop", reasons: []string{"stop", "end_turn", "stop_sequence", "completed", "complete", "eos", "finished", "STOP"}, want: aiobservabilitytypes.FinishReasonStop},
{name: "ToolCallVariants_MapToToolCall", reasons: []string{"tool_call", "tool_calls", "tool_use", "function_call"}, want: aiobservabilitytypes.FinishReasonToolCall},
{name: "TokenLimits_MapToLength", reasons: []string{"length", "max_tokens", "max_output_tokens", "max_completion_tokens", "model_length", "MAX_TOKENS"}, want: aiobservabilitytypes.FinishReasonLength},
{name: "SafetyBlocks_MapToContentFilter", reasons: []string{"content_filter", "content_filtered", "guardrail_intervened", "safety", "refusal", "recitation", "blocklist", "prohibited_content", "spii", "SAFETY"}, want: aiobservabilitytypes.FinishReasonContentFilter},
{name: "Failures_MapToError", reasons: []string{"error", "failed", "incomplete"}, want: aiobservabilitytypes.FinishReasonError},
{name: "UnknownReason_KeptLowercased", reasons: []string{"Weird"}, want: "weird"},
{name: "EmptyReason_StaysEmpty", reasons: []string{"", " "}, want: ""},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
for _, reason := range testCase.reasons {
assert.Equal(t, testCase.want, normalizeFinishReason(reason), reason)
}
})
}
}

View File

@@ -1 +0,0 @@
[{"role":"user","content":"What's the weather in Bengaluru? Use the tool."},{"role":"assistant","content":"","tool_calls":[{"id":"call_yGnJsGOZMnHKTA2x1XM8897b","type":"function","name":"get_current_weather","args":"{\"city\":\"Bengaluru\"}"}]},{"role":"tool","content":"{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]

View File

@@ -1,33 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_yGnJsGOZMnHKTA2x1XM8897b",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role":"assistant","content":"","tool_calls":[{"id":"call_yGnJsGOZMnHKTA2x1XM8897b","type":"function","name":"get_current_weather","args":"{\"city\":\"Bengaluru\"}"}]}]

View File

@@ -1,15 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_yGnJsGOZMnHKTA2x1XM8897b",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_iXrmKomEbGycID5aX4BYrQcg", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_iXrmKomEbGycID5aX4BYrQcg", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_iXrmKomEbGycID5aX4BYrQcg",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_iXrmKomEbGycID5aX4BYrQcg"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_iXrmKomEbGycID5aX4BYrQcg", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_iXrmKomEbGycID5aX4BYrQcg",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"type": "text", "content": "What's the weather in Bengaluru?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_mnRhZ0fYlvrSujkEcdmpppNw", "name": "get_weather", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_mnRhZ0fYlvrSujkEcdmpppNw", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_mnRhZ0fYlvrSujkEcdmpppNw",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_mnRhZ0fYlvrSujkEcdmpppNw"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "id": "call_mnRhZ0fYlvrSujkEcdmpppNw", "name": "get_weather", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_mnRhZ0fYlvrSujkEcdmpppNw",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
{"generations":[[{"text":"","generation_info":{"finish_reason":"tool_calls","logprobs":null},"type":"ChatGeneration","message":{"lc":1,"type":"constructor","id":["langchain","schema","messages","AIMessage"],"kwargs":{"content":"","additional_kwargs":{"refusal":null},"response_metadata":{"token_usage":{"completion_tokens":17,"prompt_tokens":63,"total_tokens":80,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"model_provider":"openai","model_name":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_5feb5fe6c2","id":"chatcmpl-Dm101OqaY01WKntqj29Epz5er05ry","service_tier":"default","finish_reason":"tool_calls","logprobs":null},"type":"ai","id":"lc_run--019e844a-d27a-7c23-b1ed-a542e0f855da-0","tool_calls":[{"name":"get_weather","args":{"city":"Bengaluru"},"id":"call_nbrbkc5fom95q8uMA2bCvH6s","type":"tool_call"}],"usage_metadata":{"input_tokens":63,"output_tokens":17,"total_tokens":80,"input_token_details":{"audio":0,"cache_read":0},"output_token_details":{"audio":0,"reasoning":0}},"invalid_tool_calls":[]}}}]],"llm_output":{"token_usage":{"completion_tokens":17,"prompt_tokens":63,"total_tokens":80,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"model_provider":"openai","model_name":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_5feb5fe6c2","id":"chatcmpl-Dm101OqaY01WKntqj29Epz5er05ry","service_tier":"default"},"run":null,"type":"LLMResult"}

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_nbrbkc5fom95q8uMA2bCvH6s",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
{"messages":[[{"lc":1,"type":"constructor","id":["langchain","schema","messages","SystemMessage"],"kwargs":{"content":"You are a weather assistant. Call get_weather once per city.","type":"system"}},{"lc":1,"type":"constructor","id":["langchain","schema","messages","HumanMessage"],"kwargs":{"content":"What's the weather in Bengaluru?","type":"human"}},{"lc":1,"type":"constructor","id":["langchain","schema","messages","AIMessage"],"kwargs":{"content":"","additional_kwargs":{"refusal":null},"response_metadata":{"token_usage":{"completion_tokens":17,"prompt_tokens":63,"total_tokens":80,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"model_provider":"openai","model_name":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_5feb5fe6c2","id":"chatcmpl-Dm101OqaY01WKntqj29Epz5er05ry","service_tier":"default","finish_reason":"tool_calls","logprobs":null},"type":"ai","id":"lc_run--019e844a-d27a-7c23-b1ed-a542e0f855da-0","tool_calls":[{"name":"get_weather","args":{"city":"Bengaluru"},"id":"call_nbrbkc5fom95q8uMA2bCvH6s","type":"tool_call"}],"usage_metadata":{"input_tokens":63,"output_tokens":17,"total_tokens":80,"input_token_details":{"audio":0,"cache_read":0},"output_token_details":{"audio":0,"reasoning":0}},"invalid_tool_calls":[]}},{"lc":1,"type":"constructor","id":["langchain","schema","messages","ToolMessage"],"kwargs":{"content":"{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}","type":"tool","tool_call_id":"call_nbrbkc5fom95q8uMA2bCvH6s","status":"success"}}]]}

View File

@@ -1,43 +0,0 @@
[
{
"role": "system",
"content": [
{
"type": "text",
"content": "You are a weather assistant. Call get_weather once per city."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_nbrbkc5fom95q8uMA2bCvH6s",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_nbrbkc5fom95q8uMA2bCvH6s"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"type": "text", "content": "What's the weather in Bengaluru? Use the tool."}]}, {"role": "assistant", "parts": [], "tool_calls": [{"function": {"arguments": "{\"city\":\"Bengaluru\"}", "name": "get_current_weather"}, "id": "call_GuU9siyYfjnx3DfHKzFbRrvD", "type": "function"}]}, {"role": "tool", "parts": [{"type": "text", "content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}], "tool_call_id": "call_GuU9siyYfjnx3DfHKzFbRrvD"}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_GuU9siyYfjnx3DfHKzFbRrvD",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_GuU9siyYfjnx3DfHKzFbRrvD"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "text", "content": "The animal in the image is a cartoon duck."}], "finish_reason": "stop"}]

View File

@@ -1,12 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "text",
"content": "The animal in the image is a cartoon duck."
}
],
"finishReason": "stop"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_itnlpgQcfe0fb0nLJym7DTnt", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_itnlpgQcfe0fb0nLJym7DTnt", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_itnlpgQcfe0fb0nLJym7DTnt",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_itnlpgQcfe0fb0nLJym7DTnt"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_itnlpgQcfe0fb0nLJym7DTnt", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_itnlpgQcfe0fb0nLJym7DTnt",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role":"system","content":"You are a weather assistant. When asked about weather, call get-weather once per city and summarise."},{"role":"user","content":[{"type":"text","text":"What is the weather in Bengaluru?"}]},{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_526phoykytobsvLouzES805J","toolName":"getWeather","args":{"city":"Bengaluru"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_526phoykytobsvLouzES805J","toolName":"getWeather","result":{"city":"Bengaluru","temperatureC":18,"summary":"Clear"}}]}]

View File

@@ -1,44 +0,0 @@
[
{
"role": "system",
"content": [
{
"type": "text",
"content": "You are a weather assistant. When asked about weather, call get-weather once per city and summarise."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"content": "What is the weather in Bengaluru?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_526phoykytobsvLouzES805J",
"name": "getWeather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\":\"Bengaluru\",\"summary\":\"Clear\",\"temperatureC\":18}",
"name": "getWeather",
"toolCallId": "call_526phoykytobsvLouzES805J"
}
]
}
]

View File

@@ -1 +0,0 @@
The current weather in Bengaluru is clear with a temperature of 18°C.

View File

@@ -1,10 +0,0 @@
[
{
"content": [
{
"type": "text",
"content": "The current weather in Bengaluru is clear with a temperature of 18°C."
}
]
}
]

View File

@@ -1 +0,0 @@
[{"toolCallType":"function","toolCallId":"call_526phoykytobsvLouzES805J","toolName":"getWeather","args":"{\"city\":\"Bengaluru\"}"}]

View File

@@ -1,15 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_526phoykytobsvLouzES805J",
"name": "getWeather",
"arguments": {
"city": "Bengaluru"
}
}
]
}
]

View File

@@ -1 +0,0 @@
{"include": [], "input": [{"content": "What's the weather in Bangalore right now?", "role": "user"}, {"arguments": "{\"city\":\"Bangalore\"}", "call_id": "call_MQTQ0XYL95vA5k31cbCgRt5y", "name": "get_weather", "type": "function_call", "id": "fc_0368c3e62fb9ae12006a199f7c14c481a1a22444594cc574e4", "status": "completed"}, {"call_id": "call_MQTQ0XYL95vA5k31cbCgRt5y", "output": "27C, partly cloudy, light breeze from the south.", "type": "function_call_output"}], "instructions": "You are a concise weather assistant. When asked about weather, call the get_weather tool exactly once per city, then summarise the result in one sentence.", "model": "gpt-4o-mini", "tools": [{"name": "get_weather", "parameters": {"properties": {"city": {"title": "City", "type": "string"}}, "required": ["city"], "title": "get_weather_args", "type": "object", "additionalProperties": false}, "strict": true, "type": "function", "description": "Look up the current weather for a city. Returns a short summary string."}]}

View File

@@ -1,43 +0,0 @@
[
{
"role": "system",
"content": [
{
"type": "text",
"content": "You are a concise weather assistant. When asked about weather, call the get_weather tool exactly once per city, then summarise the result in one sentence."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bangalore right now?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_MQTQ0XYL95vA5k31cbCgRt5y",
"name": "get_weather",
"arguments": {
"city": "Bangalore"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "27C, partly cloudy, light breeze from the south.",
"toolCallId": "call_MQTQ0XYL95vA5k31cbCgRt5y"
}
]
}
]

View File

@@ -1 +0,0 @@
{"id":"resp_017c5eb995d99968006a199f9b483481979ca790db905506fc","created_at":1780064155.0,"error":null,"incomplete_details":null,"instructions":"You solve problems step by step and give a final answer.","metadata":{},"model":"o3-mini-2025-01-31","object":"response","output":[{"id":"rs_017c5eb995d99968006a199f9bff248197978bf9193f34c29a","summary":[],"type":"reasoning"},{"id":"msg_017c5eb995d99968006a199f9c8dcc81979f4e10afbe5350a1","content":[{"annotations":[],"text":"Step 1: Let the cost of the ball be x dollars.\nStep 2: Since the bat costs $1 more than the ball, its cost is x + 1 dollars.\nStep 3: According to the problem, the total cost of the bat and ball is $1.10. Set up the equation:\n  x + (x + 1) = 1.10\nStep 4: Simplify the equation:\n  2x + 1 = 1.10\nStep 5: Subtract 1 from both sides:\n  2x = 0.10\nStep 6: Divide both sides by 2:\n  x = 0.05\n\nFinal Answer: The ball costs $0.05.","type":"output_text","logprobs":[]}],"role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":true,"temperature":1.0,"tool_choice":"auto","tools":[],"top_p":1.0,"background":false,"completed_at":1780064157.0,"max_output_tokens":null,"max_tool_calls":null,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":"medium","summary":null,"context":"current_turn"},"safety_identifier":null,"service_tier":"default","status":"completed","text":{"format":{"type":"text"},"verbosity":"medium"},"top_logprobs":0,"truncation":"disabled","usage":{"input_tokens":49,"input_tokens_details":{"cached_tokens":0},"output_tokens":431,"output_tokens_details":{"reasoning_tokens":128},"total_tokens":480},"user":null,"billing":{"payer":"developer"},"frequency_penalty":0.0,"moderation":null,"presence_penalty":0.0,"store":true}

View File

@@ -1,20 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "thinking"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"content": "Step 1: Let the cost of the ball be x dollars.\nStep 2: Since the bat costs $1 more than the ball, its cost is x + 1 dollars.\nStep 3: According to the problem, the total cost of the bat and ball is $1.10. Set up the equation:\n  x + (x + 1) = 1.10\nStep 4: Simplify the equation:\n  2x + 1 = 1.10\nStep 5: Subtract 1 from both sides:\n  2x = 0.10\nStep 6: Divide both sides by 2:\n  x = 0.05\n\nFinal Answer: The ball costs $0.05."
}
],
"finishReason": "stop"
}
]

View File

@@ -1 +0,0 @@
{"messages": [{"role": "user", "content": "What's the weather in Paris and in London? Use the tool for each."}, {"content": null, "refusal": null, "role": "assistant", "annotations": [], "audio": null, "function_call": null, "tool_calls": [{"id": "call_SW4hst7nzysgvWSAz4OdyK9D", "function": {"arguments": "{\"city\": \"Paris\"}", "name": "get_current_weather"}, "type": "function"}, {"id": "call_si8wiETYGpooMB7gMXSgMQpk", "function": {"arguments": "{\"city\": \"London\"}", "name": "get_current_weather"}, "type": "function"}]}, {"role": "tool", "tool_call_id": "call_SW4hst7nzysgvWSAz4OdyK9D", "content": "{\"city\": \"Paris\", \"temp_c\": 18, \"summary\": \"Clear\"}"}, {"role": "tool", "tool_call_id": "call_si8wiETYGpooMB7gMXSgMQpk", "content": "{\"city\": \"London\", \"temp_c\": 18, \"summary\": \"Clear\"}"}], "model": "gpt-4o-mini", "max_tokens": 80}

View File

@@ -1,52 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Paris and in London? Use the tool for each."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_SW4hst7nzysgvWSAz4OdyK9D",
"name": "get_current_weather",
"arguments": {
"city": "Paris"
}
},
{
"type": "tool_call",
"id": "call_si8wiETYGpooMB7gMXSgMQpk",
"name": "get_current_weather",
"arguments": {
"city": "London"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Paris\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_SW4hst7nzysgvWSAz4OdyK9D"
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"London\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_si8wiETYGpooMB7gMXSgMQpk"
}
]
}
]

View File

@@ -1 +0,0 @@
{"id":"chatcmpl-DksB6cYbMPwTBkJixOAuxKGZpQKds","choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"content":null,"refusal":null,"role":"assistant","annotations":[],"tool_calls":[{"id":"call_MdYKTRrkshuBG9W5oBXYZaRc","function":{"arguments":"{\"city\":\"Bengaluru\",\"unit\":\"c\"}","name":"get_current_weather"},"type":"function"}]}}],"created":1780063720,"model":"gpt-4o-mini-2024-07-18","object":"chat.completion","service_tier":"default","system_fingerprint":"fp_c6aa0586c9","usage":{"completion_tokens":22,"prompt_tokens":65,"total_tokens":87,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}}}

View File

@@ -1,17 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_MdYKTRrkshuBG9W5oBXYZaRc",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru",
"unit": "c"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_weather", "id": "call_TaqNOsbmsz4ifWLVyDHYAAEK", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_TaqNOsbmsz4ifWLVyDHYAAEK", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_TaqNOsbmsz4ifWLVyDHYAAEK",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_TaqNOsbmsz4ifWLVyDHYAAEK"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_weather", "id": "call_TaqNOsbmsz4ifWLVyDHYAAEK", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_TaqNOsbmsz4ifWLVyDHYAAEK",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
{"completion":"Let x be the cost of the ball. Then the bat costs x + 1 dollars. The total cost is given as:\n\n  x + (x + 1) = 1.10\n\nCombine like terms:\n\n  2x + 1 = 1.10\n\nSubtract 1 from both sides:\n\n  2x = 0.10\n\nDivide both sides by 2:\n\n  x = 0.05\n\nSo, the ball costs 5 cents.","reasoning":"**Calculating ball price**\n\nI'm determining the price of the ball based on a scenario where a bat and ball combined cost $1.10, with the bat costing $1 more than the ball. I set up the equation: let the cost of the ball be x, making the bat x + 1. Solving the equation x + (x + 1) = 1.10 leads me to find that x equals $0.05 for the ball. I need to confirm that this works: the ball at $0.05 and the bat at $1.05 totals $1.10.","rawRequest":{"model":"openai/o3-mini","max_completion_tokens":2000}}

View File

@@ -1,15 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "thinking",
"content": "**Calculating ball price**\n\nI'm determining the price of the ball based on a scenario where a bat and ball combined cost $1.10, with the bat costing $1 more than the ball. I set up the equation: let the cost of the ball be x, making the bat x + 1. Solving the equation x + (x + 1) = 1.10 leads me to find that x equals $0.05 for the ball. I need to confirm that this works: the ball at $0.05 and the bat at $1.05 totals $1.10."
},
{
"type": "text",
"content": "Let x be the cost of the ball. Then the bat costs x + 1 dollars. The total cost is given as:\n\n  x + (x + 1) = 1.10\n\nCombine like terms:\n\n  2x + 1 = 1.10\n\nSubtract 1 from both sides:\n\n  2x = 0.10\n\nDivide both sides by 2:\n\n  x = 0.05\n\nSo, the ball costs 5 cents."
}
]
}
]

View File

@@ -1 +0,0 @@
{"messages":[{"role":"user","content":"What's the weather in Bengaluru? Use the tool."},{"content":null,"refusal":null,"role":"assistant","annotations":null,"audio":null,"function_call":null,"tool_calls":[{"id":"call_AwiGdiFjghGVhbmbGtt92rjs","function":{"arguments":"{\"city\":\"Bengaluru\"}","name":"get_current_weather"},"type":"function","index":0}],"reasoning":null},{"role":"tool","tool_call_id":"call_AwiGdiFjghGVhbmbGtt92rjs","content":"{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_AwiGdiFjghGVhbmbGtt92rjs",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_AwiGdiFjghGVhbmbGtt92rjs"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_4BfNpuiPppRa5N1LQIkuBknW", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_4BfNpuiPppRa5N1LQIkuBknW", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_4BfNpuiPppRa5N1LQIkuBknW",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_4BfNpuiPppRa5N1LQIkuBknW"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_4BfNpuiPppRa5N1LQIkuBknW", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_4BfNpuiPppRa5N1LQIkuBknW",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_dYNkfcNlSN2A0TPAkSGXDPJb", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_dYNkfcNlSN2A0TPAkSGXDPJb", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_dYNkfcNlSN2A0TPAkSGXDPJb",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_dYNkfcNlSN2A0TPAkSGXDPJb"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_dYNkfcNlSN2A0TPAkSGXDPJb", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_dYNkfcNlSN2A0TPAkSGXDPJb",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role":"user","content":[{"type":"text","text":"What is the weather in Bengaluru in celsius?"}]},{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_l0ycKcmt1uPCdGWR4SkwoLBv","toolName":"get_current_weather","args":{"city":"Bengaluru","unit":"c"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_l0ycKcmt1uPCdGWR4SkwoLBv","toolName":"get_current_weather","result":{"city":"Bengaluru","unit":"c","temperature":27,"condition":"partly cloudy"}}]}]

View File

@@ -1,36 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What is the weather in Bengaluru in celsius?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_l0ycKcmt1uPCdGWR4SkwoLBv",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru",
"unit": "c"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\":\"Bengaluru\",\"condition\":\"partly cloudy\",\"temperature\":27,\"unit\":\"c\"}",
"name": "get_current_weather",
"toolCallId": "call_l0ycKcmt1uPCdGWR4SkwoLBv"
}
]
}
]

View File

@@ -1 +0,0 @@
Hello! How are you today?

View File

@@ -1,10 +0,0 @@
[
{
"content": [
{
"type": "text",
"content": "Hello! How are you today?"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"type":"tool-call","toolCallId":"call_l0ycKcmt1uPCdGWR4SkwoLBv","toolName":"get_current_weather","args":{"city":"Bengaluru","unit":"c"}}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_l0ycKcmt1uPCdGWR4SkwoLBv",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru",
"unit": "c"
}
}
]
}
]

View File

@@ -1,160 +0,0 @@
package genaimessages
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// parseArguments decodes JSON-encoded arguments; anything else is returned as is.
func parseArguments(value any) any {
s, ok := value.(string)
if !ok {
return value
}
var decoded any
if err := json.Unmarshal([]byte(s), &decoded); err != nil {
return s
}
return decoded
}
func decodeJSONStrings(list []any) ([]any, bool) {
out := make([]any, 0, len(list))
for _, item := range list {
s, ok := item.(string)
if !ok {
return nil, false
}
var decoded map[string]any
if err := json.Unmarshal([]byte(s), &decoded); err != nil || decoded == nil {
return nil, false
}
out = append(out, decoded)
}
return out, true
}
func flattenOnce(list []any) []any {
out := make([]any, 0, len(list))
for _, item := range list {
if inner, ok := item.([]any); ok {
out = append(out, inner...)
continue
}
out = append(out, item)
}
return out
}
// object is a decoded JSON object. Its accessors return zero values for missing
// keys and for values of another type, so reads never panic.
type object map[string]any
func toObject(value any) (object, bool) {
switch v := value.(type) {
case object:
return v, true
case map[string]any:
return v, true
}
return nil, false
}
func asObject(value any) object {
m, _ := toObject(value)
return m
}
func (o object) at(key string) any {
return o[key]
}
func (o object) get(key string) (any, bool) {
value, ok := o[key]
return value, ok
}
func (o object) has(key string) bool {
_, ok := o[key]
return ok
}
func (o object) str(key string) string {
return stringOf(o[key])
}
func (o object) flag(key string) bool {
return boolOf(o[key])
}
func (o object) text(key string) (string, bool) {
s, ok := o[key].(string)
return s, ok
}
func (o object) obj(key string) (object, bool) {
return toObject(o[key])
}
func (o object) list(key string) ([]any, bool) {
l, ok := o[key].([]any)
return l, ok
}
// lookup returns the first non-nil value among keys.
func (o object) lookup(keys ...string) (any, bool) {
for _, k := range keys {
if value, ok := o[k]; ok && value != nil {
return value, true
}
}
return nil, false
}
func (o object) first(keys ...string) any {
value, _ := o.lookup(keys...)
return value
}
func firstItem(list []any) any {
if len(list) == 0 {
return nil
}
return list[0]
}
// lastMessage returns nil for an empty slice.
func lastMessage(messages []aiobservabilitytypes.Message) *aiobservabilitytypes.Message {
if len(messages) == 0 {
return nil
}
return &messages[len(messages)-1]
}
func lastItem(list []any) any {
if len(list) == 0 {
return nil
}
return list[len(list)-1]
}
func boolOf(value any) bool {
b, _ := value.(bool)
return b
}
// stringOf renders nil as "" and non-strings as compact JSON.
func stringOf(value any) string {
switch v := value.(type) {
case nil:
return ""
case string:
return v
}
data, err := json.Marshal(value)
if err != nil {
return ""
}
return string(data)
}

View File

@@ -1,53 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
)
func TestObjectAccessors_NilObject_ZeroValues(t *testing.T) {
var o object
assert.Nil(t, o.at("key"))
assert.False(t, o.has("key"))
assert.Equal(t, "", o.str("key"))
assert.False(t, o.flag("key"))
assert.Nil(t, o.first("a", "b"))
_, ok := o.obj("key")
assert.False(t, ok)
_, ok = o.list("key")
assert.False(t, ok)
_, ok = o.text("key")
assert.False(t, ok)
}
func TestObjectAccessors_WrongType_ZeroValues(t *testing.T) {
o := object{"number": 1.5, "text": "hi"}
_, ok := o.obj("number")
assert.False(t, ok)
_, ok = o.list("text")
assert.False(t, ok)
_, ok = o.text("number")
assert.False(t, ok)
assert.False(t, o.flag("text"))
assert.Equal(t, "1.5", o.str("number"))
}
func TestSliceHelpers_EmptySlice_NilOrZero(t *testing.T) {
assert.Nil(t, firstItem(nil))
assert.Nil(t, lastItem([]any{}))
assert.Nil(t, lastMessage(nil))
assert.Equal(t, "b", lastItem([]any{"a", "b"}))
messages := []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleUser}, {Role: aiobservabilitytypes.MessageRoleAssistant}}
assert.Same(t, &messages[1], lastMessage(messages))
}
func TestToObject_AcceptsDecodedMapAndObject(t *testing.T) {
_, ok := toObject(map[string]any{"a": 1})
assert.True(t, ok)
_, ok = toObject(object{"a": 1})
assert.True(t, ok)
_, ok = toObject([]any{})
assert.False(t, ok)
}

View File

@@ -1,57 +0,0 @@
package aiobservabilitytypes
const (
MessageRoleSystem MessageRole = "system"
MessageRoleUser MessageRole = "user"
MessageRoleAssistant MessageRole = "assistant"
MessageRoleTool MessageRole = "tool"
)
const (
FinishReasonStop FinishReason = "stop"
FinishReasonToolCall FinishReason = "tool_call"
FinishReasonLength FinishReason = "length"
FinishReasonContentFilter FinishReason = "content_filter"
FinishReasonError FinishReason = "error"
)
const (
PartTypeText PartType = "text"
PartTypeThinking PartType = "thinking"
PartTypeToolCall PartType = "tool_call"
PartTypeToolResult PartType = "tool_result"
PartTypeGeneric PartType = "generic"
)
type MessageRole string
type FinishReason string
type PartType string
// Part is one piece of a message. Which fields are set depends on Type:
//
// text Content
// thinking Content, empty when the provider hid the reasoning
// tool_call ID, Name, Arguments
// tool_result ToolCallID, Name, Content, IsError
// generic Content (the original value, always a string)
type Part struct {
Type PartType `json:"type" required:"true"`
Content string `json:"content,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Arguments any `json:"arguments,omitempty"`
ToolCallID string `json:"toolCallId,omitempty"`
IsError bool `json:"isError,omitempty"`
}
type Message struct {
Role MessageRole `json:"role,omitempty"`
Content []Part `json:"content" required:"true" nullable:"false"`
FinishReason FinishReason `json:"finishReason,omitempty"`
}
func (PartType) Enum() []any {
return []any{PartTypeText, PartTypeThinking, PartTypeToolCall, PartTypeToolResult, PartTypeGeneric}
}

View File

@@ -49,66 +49,55 @@ func (o ListOrder) IsValid() bool {
return slices.ContainsFunc(o.Enum(), func(v any) bool { return v == o })
}
type ListRulesParams struct {
Query string `query:"query"`
// gin cannot bind a slice of valuer enums; AlertStates converts these.
States []string `query:"states"`
Sort ListSort `query:"sort"`
Order ListOrder `query:"order"`
Limit int `query:"limit"`
Offset int `query:"offset"`
// ListFilter is the rule listing state shared by the v3 list params and saved views.
type ListFilter struct {
Query string `query:"query" json:"query"`
// gin cannot bind a slice of valuer enums; GetAlertStates converts these.
States []string `query:"states" json:"states" nullable:"false"`
Sort ListSort `query:"sort" json:"sort"`
Order ListOrder `query:"order" json:"order"`
}
// Validate normalizes in place; an over-max limit is clamped, not rejected.
func (p *ListRulesParams) Validate() error {
if n := utf8.RuneCountInString(p.Query); n > MaxListQueryLen {
// Validate normalizes in place; zero sort/order get the defaults, nil states an empty slice.
func (f *ListFilter) Validate() error {
if f.States == nil {
f.States = []string{}
}
if n := utf8.RuneCountInString(f.Query); n > MaxListQueryLen {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"query cannot be longer than %d characters, got %d", MaxListQueryLen, n)
}
if p.Sort.IsZero() {
p.Sort = ListSortUpdatedAt
} else if !p.Sort.IsValid() {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid sort %q, expected one of: `updated_at`, `created_at`, `name`, `state`, `severity`", p.Sort)
}
if p.Order.IsZero() {
p.Order = ListOrderDesc
} else if !p.Order.IsValid() {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid order %q, expected `asc` or `desc`", p.Order)
}
if p.Limit == 0 {
p.Limit = DefaultListLimit
} else if p.Limit < 0 {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid limit %d, must be a positive integer", p.Limit)
} else if p.Limit > MaxListLimit {
p.Limit = MaxListLimit
}
if p.Offset < 0 {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid offset %d, must be a non-negative integer", p.Offset)
}
if _, err := p.GetAlertStates(); err != nil {
if _, err := f.GetAlertStates(); err != nil {
return err
}
if f.Sort.IsZero() {
f.Sort = ListSortUpdatedAt
} else if !f.Sort.IsValid() {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid sort %q, expected one of: `updated_at`, `created_at`, `name`, `state`, `severity`", f.Sort)
}
if f.Order.IsZero() {
f.Order = ListOrderDesc
} else if !f.Order.IsValid() {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid order %q, expected `asc` or `desc`", f.Order)
}
return nil
}
// GetAlertStates parses States; empty means no state filtering.
func (p *ListRulesParams) GetAlertStates() ([]AlertState, error) {
if len(p.States) == 0 {
func (f *ListFilter) GetAlertStates() ([]AlertState, error) {
if len(f.States) == 0 {
return nil, nil
}
states := make([]AlertState, 0, len(p.States))
for _, raw := range p.States {
states := make([]AlertState, 0, len(f.States))
for _, raw := range f.States {
state, err := parseAlertState(raw)
if err != nil {
return nil, err
@@ -127,3 +116,32 @@ func parseAlertState(raw string) (AlertState, error) {
}
return state, nil
}
type ListRulesParams struct {
ListFilter
Limit int `query:"limit"`
Offset int `query:"offset"`
}
// Validate normalizes in place; an over-max limit is clamped, not rejected.
func (p *ListRulesParams) Validate() error {
if err := p.ListFilter.Validate(); err != nil {
return err
}
if p.Limit == 0 {
p.Limit = DefaultListLimit
} else if p.Limit < 0 {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid limit %d, must be a positive integer", p.Limit)
} else if p.Limit > MaxListLimit {
p.Limit = MaxListLimit
}
if p.Offset < 0 {
return errors.NewInvalidInputf(ErrCodeRuleListInvalid,
"invalid offset %d, must be a non-negative integer", p.Offset)
}
return nil
}

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